此处主要讲的是从数据来到,中断到最终数据包被处理的过程。

首先来介绍一下IO端口访问问题,内核提供了这样一组函数处理: /kernel/io.c中

  • inb( )、inw( )、inl( )函数
    分别从I/O端口读取1、2或4个连续字节。 后缀“b”、“w”、“l”分别代表一个字节(8位)、一个字(16位)以及一个长整型(32 位)。

  • inb_p( )、inw_p( )、inl_p( )
    分别从I/O端口读取1、2或4个连续字节,然后执行一条 “空指令” 使CPU暂停。 p 可以理解成pause

  • outb( )、outw( )、outl( )
    分别向一个I/O端口写入1、2或4个连续字节。

  • outb_p( )、outw_p( )、outl_p( )
    分别向一个I/O端口写入1、2或4个连续字节,然后执行一条“空指令”指令使CPU暂停。

  • insb( )、insw( )、insl( )
    分别从I/O端口读入以1、2或4个字节为一组的连续字节序列。字节序列的长度由该函数的参数给出。

  • outsb( )、outsw( )、outsl( )
    分别向I/O端口写入以1、2或4个字节为一组的连续字节序列。

net_interrupt()

1、当一个中断来到,首先响应 net_interrupt函数

/** The typical workload of the driver:* Handle the network interface interrupts.*/
static irqreturn_t net_interrupt(int irq, void *dev_id)  // 注意参数是:中断号和设备id{struct net_device *dev = dev_id;struct net_local *np;int ioaddr, status;int handled = 0;ioaddr = dev->base_addr;       // 设备的IO地址 np = netdev_priv(dev);         // 得到dev私有数据status = inw(ioaddr + 0);      // 从端口读两个字节if (status == 0)goto out;handled = 1;if (status & RX_INTR) {/* Got a packet(s). */net_rx(dev);          // 使用这个函数net_rx来获取一个数据包 -----> receive}                             // 这个函数下面会说到#if TX_RINGif (status & TX_INTR) {       // 发送数据/* Transmit complete. */net_tx(dev);             // 发送数据使用net_tx ------> transmitnp->stats.tx_packets++;  // 计数 netif_wake_queue(dev);   // 处理结束,唤醒下一个队列中等待者                                                        }
#endif if (status & COUNTERS_INTR) {                                /* Increment the appropriate 'localstats' field. */ np->stats.tx_window_errors++; }
out: return IRQ_RETVAL(handled); // 返回中断
}

net_rx()

2、下面需要看一下接收数据包函数net_rx

/* We have a good packet(s), get it/them out of the buffers. */
static void
net_rx(struct net_device *dev)        // 所谓接收数据包,其实就是构造skb数据结构 ^_^
{struct net_local *lp = netdev_priv(dev);int ioaddr = dev->base_addr;int boguscount = 10;do { // 下面是循环接收数据么int status = inw(ioaddr);     // 获取状态int pkt_len = inw(ioaddr);    // 获取包大小if (pkt_len == 0)               /* 全部接收 */break;                  /* 可以结束 */if (status & 0x40) {    /* There was an error. */lp->stats.rx_errors++;if (status & 0x20) lp->stats.rx_frame_errors++;if (status & 0x10) lp->stats.rx_over_errors++;if (status & 0x08) lp->stats.rx_crc_errors++;if (status & 0x04) lp->stats.rx_fifo_errors++;} else {/* Malloc up new buffer. */struct sk_buff *skb;lp->stats.rx_bytes+=pkt_len;     // 接收的字节数+pkt_lenskb = dev_alloc_skb(pkt_len);    // 需要接收多少bytes就分配多少空间给sk_buffif (skb == NULL) {               // 需要丢包printk(KERN_NOTICE "%s: Memory squeeze, dropping packet.\n",dev->name);lp->stats.rx_dropped++;  // 丢包数++break;}skb->dev = dev;                  // 现在开始构建skb包/* 'skb->data' points to the start of sk_buff data area. */memcpy(skb_put(skb,pkt_len), (void*)dev->rmem_start,    // 注意开始从dev向skb中放入数据,大小pkt_lenpkt_len);/* or */insw(ioaddr, skb->data, (pkt_len + 1) >> 1);netif_rx(skb);                   // 这个函数很重要,下面会具体说~dev->last_rx = jiffies;          // 上一次rx的时间lp->stats.rx_packets++;          // 接收包数量++lp->stats.rx_bytes += pkt_len;   // 接收字节数+pkt_len}} while (--boguscount);return;}

netif_rx()

3、显然我们知道现在要分析netif_rx函数了

先看几个函数:

local_irq_disable() , local_irq_enable() , local_irq_save() 和 local_irq_restore() 为中断处理函数,

主要是在要进入临界区时禁止中断和在出临界区时使能中断。

local_irq_disable() 和 local_irq_enable() 配对使用;

local_irq_save() 则和 local_irq_restore() 配对使用。

 /***      netif_rx        -       post buffer to the network code*      @skb: buffer to post**      This function receives a packet from a device driver and queues it for*      the upper (protocol) levels to process.  It always succeeds. The buffer*      may be dropped during processing for congestion control or by the*      protocol layers.**      return values:*      NET_RX_SUCCESS  (no congestion)*      NET_RX_DROP     (packet was dropped)**///  需要注意的是:这里是非NAPI方式下的函数int netif_rx(struct sk_buff *skb) // 注意接收数据后将数据进行排队,然后给上层协议处理,不过也有可能因为拥塞之类丢包!{struct softnet_data *queue;   // 每个cpu结构都有这样一个队列,这样在SMP之间就避免了枷锁操作,提高并发度unsigned long flags;/* if netpoll wants it, pretend we never saw it */if (netpoll_rx(skb))          // 关于netpoll机制以后在讨论return NET_RX_DROP;if (!skb->tstamp.tv64)net_timestamp(skb);   // 设置包到达时间/** The code is rearranged so that the path is the most* short when CPU is congested, but is still operating.*/local_irq_save(flags);        // 关中断,禁止中断queue = &__get_cpu_var(softnet_data);  // 取得当前CPU输入队列(得到CPU参数数据队列   softnet_data)__get_cpu_var(netdev_rx_stat).total++; // 更新当前CPU接收到的帧的数量,包括接收的和丢弃的if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {  // 每个CPU都有输入队列的最大长度,如果超过,则丢弃该数据帧if (queue->input_pkt_queue.qlen) {   // 如果队列中有元素enqueue:dev_hold(skb->dev);          // 网络设备引用值++__skb_queue_tail(&queue->input_pkt_queue, skb);  // 将skb添加到队列的末尾(注意这里产生软中断NET_RX_SOFTIRQ,进一步处理包)local_irq_restore(flags);    // 开中断   // 同时需要知道:NET_RX_SOFTIRQ 是由net_rx_action函数处理return NET_RX_SUCCESS;       // 返回接收数据成功}napi_schedule(&queue->backlog); // 如果qlen=0,说明queue->backlog可能已经当前CPU的poll-list中移除了,要重新加入goto enqueue;                  // list_add_tail(&n->poll_list, &__get_cpu_var(softnet_data).poll_list);}                                     // 其实就是让后面action中循环能够找到这个设备,,,然后goto到上面重新将包放入队列__get_cpu_var(netdev_rx_stat).dropped++;     // 如果上面的没有执行成功,那么丢包数量++local_irq_restore(flags);                    // 开中断  允许中断kfree_skb(skb); // 因为丢包才能才第到此处,所以将skb free掉 return NET_RX_DROP; // 返回丢包
}

注意一个问题: 上面在将包放进队列的过程中,是关了中断的,完成后开中断,但是在接收包的数据的时候并没有禁止中断,即收包的IRQ是不需要被禁用的。因为将包放入到cpu的等待队列不会耗时太长。这也说明,传统API只能适用与低速设备。

简介:在没有NAPI的时候,都是通过中断系统来处理包的到达,这就才造成一个问题,当有很多很多短包蜂拥到达的时候,中断系统将会忙死,所以为了优化这种情况,加入NAPI,其实采用的是一种轮询方式。非NAPI方式是将数据放进CPU的队列中,而NAPI是有自己的私有队列的,可以说是自己的私有缓冲区!!!

下面来理清一下思路,在内核初始化的时候,对于每个CPU中的softnet_data都初始化了

net_dev_init()

static int __init net_dev_init(void)
{int i, rc = -ENOMEM;BUG_ON(!dev_boot_phase);if (dev_proc_init())                      // 不管goto out;if (netdev_kobject_init())                // 不管goto out; INIT_LIST_HEAD(&ptype_all);for (i = 0; i < PTYPE_HASH_SIZE; i++)     // 不管INIT_LIST_HEAD(&ptype_base[i]);if (register_pernet_subsys(&netdev_net_ops))        // 不管goto out;if (register_pernet_device(&default_device_ops))    // 不管goto out;/**      Initialise the packet receive queues.初始化话数据包的接收队列*/for_each_possible_cpu(i) {            // 对于每一个CPU都会进行处理struct softnet_data *queue;   // 每个CPU中都有这样一个结构queue = &per_cpu(softnet_data, i);   // 获得这个iCPU上面的softnet_data结构skb_queue_head_init(&queue->input_pkt_queue);  // 初始化接收数据队列queue->completion_queue = NULL;      // 暂无完成INIT_LIST_HEAD(&queue->poll_list);   // 初始化设备队列(注意poll_list在处理数据的时候会被遍历)queue->backlog.poll = process_backlog; // 这个很重要!在以后的处理这个设备上的数据的时候使用这个函数,,,看下面queue->backlog.weight = weight_p;}netdev_dma_register();             // 下面忽略dev_boot_phase = 0;open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);hotcpu_notifier(dev_cpu_callback, 0);dst_init();dev_mcast_init();rc = 0;out:return rc;}

process_backlog()

看看process_backlog函数:

static int process_backlog(struct napi_struct *napi, int quota)  // 注意需要在下面更详细地说
{int work = 0;struct softnet_data *queue = &__get_cpu_var(softnet_data);unsigned long start_time = jiffies;napi->weight = weight_p;do {struct sk_buff *skb;struct net_device *dev;local_irq_disable();skb = __skb_dequeue(&queue->input_pkt_queue);   // 从队里获取一个skbif (!skb) {__napi_complete(napi);      // 如果队列已经空了,那么其实就是将napi的poll_list从CPU的那个结构中移除local_irq_enable();break;}local_irq_enable();dev = skb->dev;netif_receive_skb(skb);    // 下面处理接收数据(当然这里需要在下面更详细地说)dev_put(dev);} while (++work < quota && jiffies == start_time);// 需要注意的是:退出有两情况:当处理完所有skb 或者 分配时间达到 。return work;
}

struct softnet_data

// 看一下softnet_data结构体struct softnet_data{struct net_device       *output_queue;        // 网络设备发送队列的头struct sk_buff_head     input_pkt_queue;      // 接收缓冲区的sk_buff队列struct list_head        poll_list;            // poll设备队列头struct sk_buff          *completion_queue;    // 完成发送数据包,等待释放的队列struct napi_struct      backlog;              // NAPI结构#ifdef CONFIG_NET_DMAstruct dma_chan         *net_dma;#endif};

4、放进队列之后该怎么处理呢?是不是要开始处理数据了,net_rx_action现在出现!

注意接收到的数据在两个地方等待net_rx_action来处理:

  1. 对于非NAPI方式来说,我们需要从CPU的softnet_data->input_pkt_queue中取得数据。
  2. 对于NAPI方式,前面说过有自己的缓冲区,那么poll函数从设备缓存读取数据。

下面看代码:

net_rx_action()

static void net_rx_action(struct softirq_action *h)
{struct list_head *list = &__get_cpu_var(softnet_data).poll_list; // 获取设备列表unsigned long start_time = jiffies;   // 获取当前时间戳int budget = netdev_budget;void *have;local_irq_disable();                   // 禁止中断while (!list_empty(list)) {            // 对每一个设备进行循环处理一次,看是否有设备等待轮询取得数据struct napi_struct *n;int work, weight;/* If softirq window is exhuasted then punt.** Note that this is a slight policy change from the* previous NAPI code, which would allow up to 2* jiffies to pass before breaking out.  The test* used to be "jiffies - start_time > 1".*/if (unlikely(budget <= 0 || jiffies != start_time)) // 保证当前的 POLL 过程的时间不超过一个时间片,这样不至于被软中断占用太多的时间goto softnet_break;local_irq_enable();    // 开中断/* Even though interrupts have been re-enabled, this* access is safe because interrupts can only add new* entries to the tail of this list, and only ->poll()* calls can remove this head entry from the list.*/n = list_entry(list->next, struct napi_struct, poll_list); // 从softnet_data 数据结构中的轮循队列上获得等待轮循的napi_struct结构have = netpoll_poll_lock(n);   // 锁定该 struct napi_struct ,并且记录当前调度的CPUweight = n->weight;/* This NAPI_STATE_SCHED test is for avoiding a race* with netpoll's poll_napi().  Only the entity which* obtains the lock and sees NAPI_STATE_SCHED set will* actually make the ->poll() call.  Therefore we avoid* accidently calling ->poll() when NAPI is not scheduled.*/work = 0;if (test_bit(NAPI_STATE_SCHED, &n->state)) // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!work = n->poll(n, weight);         // !!!这里相当重要!根据weight调用想要的poll函数!// 之前说过如果低非NAPI,那么使用的是初始化时候的即process_backlog函数WARN_ON_ONCE(work > weight);               // 如果是NAPI函数,那么就是自己的poll函数处理      // 那么又要返回上面看process_backlog函数(往下看~~~~有重写)budget -= work;local_irq_disable();/* Drivers must not modify the NAPI state if they* consume the entire weight.  In such cases this code* still "owns" the NAPI instance and therefore can* move the instance around on the list at-will.*/if (unlikely(work == weight)) {if (unlikely(napi_disable_pending(n)))__napi_complete(n);elselist_move_tail(&n->poll_list, list);}netpoll_poll_unlock(have);}out:local_irq_enable();#ifdef CONFIG_NET_DMA/** There may not be any more sk_buffs coming right now, so push* any pending DMA copies to hardware*/if (!cpus_empty(net_dma.channel_mask)) {int chan_idx;for_each_cpu_mask(chan_idx, net_dma.channel_mask) {struct dma_chan *chan = net_dma.channels[chan_idx];if (chan)dma_async_memcpy_issue_pending(chan);}}#endifreturn;softnet_break:__get_cpu_var(netdev_rx_stat).time_squeeze++;__raise_softirq_irqoff(NET_RX_SOFTIRQ);goto out;}

注意看下面的部分代码:基本的意思就是从CPU这个softnet_data的字段input_pkt_queue队列中不断的取和当前napi_struct相关的数据包,每次获取一个数据包那么就使用函数netif_receive_skb函数处理!这个函数也是非常重要的!下面再说…如果没有的话,那么就是__napi_complete函数将这个napi_struct移除polllist,以免下次被循环到没有数据。

do {struct sk_buff *skb;struct net_device *dev;local_irq_disable();skb = __skb_dequeue(&queue->input_pkt_queue);    // 出来一个数据if (!skb) {                            // 如果是null,那么队列空,移除设备__napi_complete(napi);local_irq_enable();break;}local_irq_enable();dev = skb->dev;                 // 获取这个包对应的设备netif_receive_skb(skb);         // 这个函数最重要!下面分析!!!!!!!!dev_put(dev);
} while (++work < quota && jiffies == start_time);

看netif_receive_skb函数!netif_receive_skb是链路层接收数据报的最后一站!!!

netif_receive_skb()

/***      netif_receive_skb - process receive buffer from network*      @skb: buffer to process**      netif_receive_skb() is the main receive data processing function.*      It always succeeds. The buffer may be dropped during processing*      for congestion control or by the protocol layers.**      This function may only be called from softirq context and interrupts*      should be enabled.**      Return values (usually ignored):*      NET_RX_SUCCESS: no congestion*      NET_RX_DROP: packet was dropped*/
int netif_receive_skb(struct sk_buff *skb)   // 注意这个函数可能要被很多人处理,因为可以注册多个协议进行处理
{struct packet_type *ptype, *pt_prev;struct net_device *orig_dev;int ret = NET_RX_DROP;__be16 type;/* if we've gotten here through NAPI, check netpoll */if (netpoll_receive_skb(skb))return NET_RX_DROP;if (!skb->tstamp.tv64)net_timestamp(skb);     // 更新时间if (!skb->iif)                  // 设备的(idx)编号skb->iif = skb->dev->ifindex;orig_dev = skb_bond(skb);       // 可以展开成  orig_dev = skb->dev;skb->dev = skb->dev->master;// 不是很懂~  (处理路由聚合问题)if (!orig_dev)return NET_RX_DROP;__get_cpu_var(netdev_rx_stat).total++;   // cpu统计skb_reset_network_header(skb);              // 网络层头(校准头指针)skb_reset_transport_header(skb);            // 传输层头(校准头指针)skb->mac_len = skb->network_header - skb->mac_header;   // 注意mac层长度就是网络层的头---->mac层头之间部分!pt_prev = NULL;rcu_read_lock();#ifdef CONFIG_NET_CLS_ACTif (skb->tc_verd & TC_NCLS) {skb->tc_verd = CLR_TC_NCLS(skb->tc_verd);goto ncls;}#endif     // 下面类似于协议嗅探器,因为是ETH_p_all类型// 这位部分代码是核心代码哦!  以下的代码用于在协议链上寻找匹配的协议(在ptype_all中找)list_for_each_entry_rcu(ptype, &ptype_all, list) {   // 这里需要先理解一下packet_type结构体,goto到下面先看看!!!!if (!ptype->dev || ptype->dev == skb->dev) { // 这个地方在下面有解释if (pt_prev)ret = deliver_skb(skb, pt_prev, orig_dev); // 此处找到的是ETH_P_ALL类型协议(如果有注册)pt_prev = ptype;}}#ifdef CONFIG_NET_CLS_ACTskb = handle_ing(skb, &pt_prev, &ret, orig_dev);if (!skb)goto out;ncls:#endif// 若编译内核时选上BRIDGE,下面会执行网桥模块skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);  // 进入桥进行二层处理,如果返回skb == NULL,说明skb 被直接二层转发走了,不用再送网络层了,函数直接返回if (!skb)    // 包是否被桥转发走了   ( 具体的后来在分析 )goto out;skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev); // 编译内核时选上MAC_VLAN模块,下面才会执行if (!skb)                        // 同样如果被vlan消耗,那么无需往上面协议层传递了~!直接退出返回goto out;// 注意哦:如果数据包在上面没有被处理掉,那么说明要传递到上面一层即ip层进行处理                                                                           // 注意在I派层处理有两种情况:还要往上面一层即TCP层传递,或者直接ARP处理// 这位部分代码是核心代码哦!  以下的代码用于在协议链上寻找匹配的协(在ptype_base  hash表中找)type = skb->protocol;list_for_each_entry_rcu(ptype,&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {   // 这里面匹配的类型就是ip层一些协议的类型if (ptype->type == type &&(!ptype->dev || ptype->dev == skb->dev)) {if (pt_prev)ret = deliver_skb(skb, pt_prev, orig_dev);     // 进行处理~~~~pt_prev = ptype;}}if (pt_prev) {ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);} else {kfree_skb(skb);/* Jamal, now you will not able to escape explaining* me how you were going to use this. :-)*/ret = NET_RX_DROP;}out:rcu_read_unlock();return ret;}

packet_type 结构体看看

struct packet_type

struct packet_type {__be16      type;   /* This is really htons(ether_type). */ // 成员保存了二层协议类型,ETH_P_IP、ETH_P_ARP,ETH_P_ALLstruct net_device  *dev;   /* NULL is wildcarded here           */int                (*func) (struct sk_buff *,          // 成员就是钩子函数了,如 ip_rcv()、arp_rcv()等等struct net_device *,struct packet_type *,struct net_device *);struct sk_buff    *(*gso_segment)(struct sk_buff *skb,int features);int               (*gso_send_check)(struct sk_buff *skb);void              *af_packet_priv;struct list_head   list;
};

注意:所有协议的packet_type存放在两条协议链中,ptype_base和ptype_all,ptype_base 为哈希链表,ptype_all为双向链.

系统使用dev_add_pack函数将指定协议类型的packet_type添加到这两个表中。

  • 对于ETH_P_ALL类型的数据报文将在ptype_all表中找到自己对应的packet_type结构。

    系统只有创建了一个PF_PACKE类型的socket才会将一个packet_type结构加到ptype_all链表中。

  • 对于ETH_P_IP和ETH_P_ARP可以在ptype_base中找到自己的packet_type结构。

    如果协议类型是ETH_P_IP那么func函数就是ip_rcv

    如果协议类型是ETH_P_ARP那么func函数就是arp_rcv

OK,现在说说deliver_skb函数:

deliver_skb()

static inline int deliver_skb(struct sk_buff *skb,struct packet_type *pt_prev,struct net_device *orig_dev)
{atomic_inc(&skb->users);return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);    // 调用的还是对应的不同协议的func函数
}

最终还是调用了func函数了,下面注意:主要说将数据包传递给ip层进行处理,所以看看 ip_rcv

ip_rcv是怎么和ETH_P_IP关联起来的,这个我们上面说过这个packet_type结构,这个结构是保存不同协议和自己的处理函数func的,那么这个结构体有自己的处理方法:

static struct packet_type arp_packet_type __read_mostly = {.type = cpu_to_be16(ETH_P_ARP),.func = arp_rcv,    // 关联上
};
static struct packet_type ip_packet_type __read_mostly = {.type = cpu_to_be16(ETH_P_IP),.func = ip_rcv,    // 关联上.gso_send_check = inet_gso_send_check,.gso_segment = inet_gso_segment,.gro_receive = inet_gro_receive,.gro_complete = inet_gro_complete,
};

下面就来看看ip_rcv函数,请看下一篇博客

原文链接:https://blog.csdn.net/shanshanpt/article/details/20377657

linux 内核网络协议栈--数据从接收到IP层(二)相关推荐

  1. 一文讲解Linux 内核网络协议栈-数据从接收到ip层

    [推荐阅读] 一文了解Linux上TCP的几个内核参数调优 一文剖析Linux内核中内存管理 分析linux启动内核源码 此处主要讲的是从数据来到,中断到最终数据包被处理的过程. 0:首先来介绍一下I ...

  2. linux内核网络协议栈--数据包的网卡缓冲区(二十四)

    程序员可能关心的基本网卡知识 网卡相关介绍:http://www.linuxidc.com/Linux/2012-12/77132.htm 一.什么是网卡? 它是主机的网络设备,本身是LAN(局域网) ...

  3. linux内核网络协议栈--数据包的接收过程(二十)

    本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的. 本文只讨论以太网的物理网卡,不涉及虚拟设备,并且以一个UDP包的接收过程作为示例. 本示例里列出的函数调用关系来自于kerne ...

  4. linux内核网络协议栈--数据包的接收过程(二十二)

    与其说这篇文章分析了网卡驱动中中数据包的接收,还不如说基于Kernel:2.6.12,以e100为例,对网卡驱动编写的一个说明.当然,对数据包的接收说的很清楚. 一.从网卡说起 这并非是一个网卡驱动分 ...

  5. linux内核网络协议栈--数据包的发送过程(二十一)

    继上一篇介绍了数据包的接收过程后,本文将介绍在Linux系统中,数据包是如何一步一步从应用程序到网卡并最终发送出去的. socket层 +-------------+| Application |+- ...

  6. linux内核网络协议栈--数据包的接收流程(二十三)

    网卡在接受数据包时会产生中断,即当 有一个以太网帧到来时,网卡向内核产生一次中断: CPU收到中断信号后,执行中断处理程序,中断处理程序会设置 缓冲区地址.DMA 地址等信息: 网卡通过DMA 方式将 ...

  7. linux内核网络协议栈--数据接收流程图(五)

    各层主要函数以及位置功能说明: 1)sock_read:初始化msghdr{}的结构类型变量msg,并且将需要接收的数据存放的地址传给msg.msg_iov->iov_base. net/soc ...

  8. linux内核网络协议栈--数据包的skb桥转发蓝图(二十六)

    话不多说,先看一张桥转发时函数调用的一个基本蓝图. 这张图中,简单的展示了,数据的接收和发送,其中还包括netfilet的钩子点所处的位置. 需要说明的是: 1.我们先暂时忽略数据包从一开始是怎么从驱 ...

  9. linux内核网络协议栈--数据包的网卡驱动收发包过程(二十五)

    网卡 网卡工作在物理层和数据链路层,主要由PHY/MAC芯片.Tx/Rx FIFO.DMA等组成,其中网线通过变压器接PHY芯片.PHY芯片通过MII接MAC芯片.MAC芯片接PCI总线 PHY芯片主 ...

最新文章

  1. 强强联合!Papers with Code携手arXiv,上传论文、提交代码一步到位
  2. 基础知识《十》unchecked异常和checked异常
  3. maven各个属性参数详解
  4. C语言实用算法系列之学生管理系统_对整个结构体操作_选择排序_提取排序规则
  5. mysql 锁问题 (相同索引键值或同一行或间隙锁的冲突)
  6. python小程序_小会计的实用Python小程序(三):人民币大写金额转换器
  7. CART决策树算法的Python实现(注释详细)
  8. 圆孔夫琅禾费衍射MATLAB程序,模拟夫琅禾费衍射的matlab源代码
  9. 一名软件测试工程师的日常
  10. 网络狂飙2(netspeeder2) v2.0 游戏版 怎么用
  11. js中的深拷贝和浅拷贝
  12. Stay hungry. Stay foolish.
  13. 高级查询组件下拉框联动(三)
  14. 设计模式与软件体系结构复习资料——设计模式
  15. 系统自带测试软件,Windows7自带软件测试RAID系统
  16. Tomcat到底是个啥?
  17. 常见颜色RGB颜色值
  18. Java工程师培训课(十六【新的领域】)
  19. laravel导出Excel表格提示内存超出
  20. Android Studio安装步骤

热门文章

  1. python中的format什么意思中文-python的format什么意思
  2. python 免费课程-2019年10种免费的Python学习课程
  3. python教程是用什么博客写的-Python 有哪些好的学习资料或者博客?
  4. 有道精品课python-网易词典在线翻译
  5. python自学要多久 知乎-怎么自学python,大概要多久?
  6. linux如何去掉目录背景颜色
  7. 算法竞赛入门第二版解题报告
  8. oracle11g安装中的问题
  9. 机器学习技法1-Linear Support Vector Machine
  10. chart 模板 - 每天5分钟玩转 Docker 容器技术(165)