万字图解Linux网络包接收过程


上面代码是一段udp server接收收据的逻辑。当在开发视角看的时候,只要客户端有对应的数据发送过来,服务器端执行`recv_from`后就能收到它,并把它打印出来。我们现在想知道的是,当网络包达到网卡,直到我们的`recvfrom`收到数据,这中间,究竟都发生过什么?


通过本文,你将深入理解Linux网络系统内部是如何实现的,以及各个部分之间如何交互。相信这对你的工作将会有非常大的帮助。本文基于Linux 3.10,源代码参见https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/,网卡驱动采用Intel的igb网卡举例。


友情提示,本文略长,可以先Mark后看!


一


Linux网络收包总览


在TCP/IP网络分层模型里,整个协议栈被分成了物理层、链路层、网络层,传输层和应用层。物理层对应的是网卡和网线,应用层对应的是我们常见的Nginx,FTP等等各种应用。Linux实现的是链路层、网络层和传输层这三层。  



在Linux内核实现中,链路层协议靠网卡驱动来实现,内核协议栈来实现网络层和传输层。内核对更上层的应用层提供socket接口来供用户进程访问。我们用Linux的视角来看到的TCP/IP网络分层模型应该是下面这个样子的。


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWlE4c2pMN09ndlB1cG9tRmZpYWRCVGI3UGliRlhqTjQ0YkJrSVYzRmppYjNEbEZwNVVXYTJmZWRGQS82NDA?x-oss-process=image/format,png)


 
 图1 Linux视角的网络协议栈 
 
在Linux的源代码中,网络设备驱动对应的逻辑位于`driver/net/ethernet`, 其中intel系列网卡的驱动在`driver/net/ethernet/intel`目录下。协议栈模块代码位于`kernel`和`net`目录。


内核和网络设备驱动是通过中断的方式来处理的。当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,以通知CPU来处理数据。对于网络模块来说,由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其它设备,例如鼠标和键盘的消息。因此Linux中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理。2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程全权处理。和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化,而软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。


好了,大概了解了网卡驱动、硬中断、软中断和ksoftirqd线程之后,我们在这几个概念的基础上给出一个内核收包的路径示意:


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWkNGSEwzcUZpY3dKaWJEdmV2VUNDaWJDdUVUelJCaWN5S1NkemdMcjZPOG5pYXEwZGRiTDhWc3V0eWlidy82NDA?x-oss-process=image/format,png)
 
 图2 Linux内核网络收包总览 
 
当网卡上收到数据以后,Linux中第一个工作的模块是网络驱动。网络驱动会以DMA的方式把网卡上收到的帧写到内存里。再向CPU发起一个中断,以通知CPU有数据到达。第二,当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数。网卡的中断处理函数并不做过多工作,发出软中断请求,然后尽快释放CPU。ksoftirqd检测到有软中断请求到达,调用poll开始轮询收包,收到后交由各级协议栈处理。对于UDP包来说,会被放到用户socket的接收队列中。


我们从上面这张图中已经从整体上把握到了Linux对数据包的处理过程。但是要想了解更多网络模块工作的细节,我们还得往下看。


二


Linux启动


Linux驱动,内核协议栈等等模块在具备接收网卡数据包之前,要做很多的准备工作才行。比如要提前创建好ksoftirqd内核线程,要注册好各个协议对应的处理函数,网络设备子系统要提前初始化好,网卡要启动好。只有这些都Ready之后,我们才能真正开始接收数据包。那么我们现在来看看这些准备工作都是怎么做的。  



### 2.1 创建ksoftirqd内核线程


Linux的软中断都是在专门的内核线程(ksoftirqd)中进行的,因此我们非常有必要看一下这些进程是怎么初始化的,这样我们才能在后面更准确地了解收包过程。该进程数量不是1个,而是N个,其中N等于你的机器的核数。


系统初始化的时候在kernel/smpboot.c中调用了smpboot\_register\_percpu\_thread, 该函数进一步会执行到spawn\_ksoftirqd(位于kernel/softirq.c)来创建出softirqd进程。


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWmlhd0pCNEV4VGlibWljcUJzSFdJNjZJaGJkQ0Z1bzBCVVFBbEM0UXFuWm1GdkxvY2J2TXJlMXhLUS82NDA?x-oss-process=image/format,png)
 
 图3 创建ksoftirqd内核线程 
 
相关代码如下:



//file: kernel/softirq.c
static struct smp_hotplug_thread softirq_threads = {
   .store          = &ksoftirqd,
   .thread_should_run  = ksoftirqd_should_run,
   .thread_fn      = run_ksoftirqd,
   .thread_comm        = “ksoftirqd/%u”,};



static __init int spawn_ksoftirqd(void){
   register_cpu_notifier(&cpu_nfb);

BUG_ON(smpboot_register_percpu_thread(&softirq_threads));    return 0;
}
early_initcall(spawn_ksoftirqd);


当ksoftirqd被创建出来以后,它就会进入自己的线程循环函数ksoftirqd\_should\_run和run\_ksoftirqd了。不停地判断有没有软中断需要被处理。这里需要注意的一点是,软中断不仅仅只有网络软中断,还有其它类型。



//file: include/linux/interrupt.h



enum{
   HI_SOFTIRQ=0,
   TIMER_SOFTIRQ,
   NET_TX_SOFTIRQ,
   NET_RX_SOFTIRQ,
   BLOCK_SOFTIRQ,
   BLOCK_IOPOLL_SOFTIRQ,
   TASKLET_SOFTIRQ,
   SCHED_SOFTIRQ,
   HRTIMER_SOFTIRQ,
   RCU_SOFTIRQ,  
};


### 2.2 网络子系统初始化


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWlBmTTE1UGpQSUFDWk5XREVjdWVBR1g0VFRDUDAyNjBsY2VuZkxOY04zQ2R6YWhvVGRsVzBhQS82NDA?x-oss-process=image/format,png)
 
 图4 网络子系统初始化 
 
linux内核通过调用`subsys_initcall`来初始化各个子系统,在源代码目录里你可以grep出许多对这个函数的调用。这里我们要说的是网络子系统的初始化,会执行到`net_dev_init`函数。



//file: net/core/dev.c
static int __init net_dev_init(void){
   …

for_each_possible_cpu(i) {
       struct softnet_data *sd = &per_cpu(softnet_data, i);

memset(sd, 0, sizeof(*sd));
       skb_queue_head_init(&sd->input_pkt_queue);
       skb_queue_head_init(&sd->process_queue);
       sd->completion_queue = NULL;
       INIT_LIST_HEAD(&sd->poll_list);
       …
   }
   …
   open_softirq(NET_TX_SOFTIRQ, net_tx_action);    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
}
subsys_initcall(net_dev_init);


在这个函数里,会为每个CPU都申请一个`softnet_data`数据结构,在这个数据结构里的`poll_list`是等待驱动程序将其poll函数注册进来,稍后网卡驱动初始化的时候我们可以看到这一过程。


另外open\_softirq注册了每一种软中断都注册一个处理函数。NET\_TX\_SOFTIRQ的处理函数为net\_tx\_action,NET\_RX\_SOFTIRQ的为net\_rx\_action。继续跟踪`open_softirq`后发现这个注册的方式是记录在`softirq_vec`变量里的。后面ksoftirqd线程收到软中断的时候,也会使用这个变量来找到每一种软中断对应的处理函数。



//file: kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *)){
   softirq_vec[nr].action = action;
}


### 2.3 协议栈注册


内核实现了网络层的ip协议,也实现了传输层的tcp协议和udp协议。这些协议对应的实现函数分别是ip\_rcv(),tcp\_v4\_rcv()和udp\_rcv()。和我们平时写代码的方式不一样的是,内核是通过注册的方式来实现的。Linux内核中的`fs_initcall`和`subsys_initcall`类似,也是初始化模块的入口。`fs_initcall`调用`inet_init`后开始网络协议栈注册。通过`inet_init`,将这些函数注册到了inet\_protos和ptype\_base数据结构中了。如下图:


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWlY5SlNlekp2eGxSNDYwV2o5aWNpYTJiNnRFOWlibk1Fd0x1TXppYTVqalFtcHJYbVdLaWFkUUd0aWJOdy82NDA?x-oss-process=image/format,png)
 
 图5 AF\_INET协议栈注册 
 
相关代码如下



//file: net/ipv4/af_inet.c
static struct packet_type ip_packet_type __read_mostly = {
   .type = cpu_to_be16(ETH_P_IP),
   .func = ip_rcv,};static const struct net_protocol udp_protocol = {
   .handler =  udp_rcv,
   .err_handler =  udp_err,
   .no_policy =    1,
   .netns_ok = 1,};static const struct net_protocol tcp_protocol = {
   .early_demux    =   tcp_v4_early_demux,
   .handler    =   tcp_v4_rcv,
   .err_handler    =   tcp_v4_err,
   .no_policy  =   1,    .netns_ok   =   1,
};
static int __init inet_init(void){
   …
   if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
       pr_crit(“%s: Cannot add ICMP protocol\n”, func);
   if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
       pr_crit(“%s: Cannot add UDP protocol\n”, func);
   if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
       pr_crit(“%s: Cannot add TCP protocol\n”, func);
   …    dev_add_pack(&ip_packet_type);
}


上面的代码中我们可以看到,udp\_protocol结构体中的handler是udp\_rcv,tcp\_protocol结构体中的handler是tcp\_v4\_rcv,通过inet\_add\_protocol被初始化了进来。



int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){
   if (!prot->netns_ok) {
       pr_err(“Protocol %u is not namespace aware, cannot register.\n”,
           protocol);
       return -EINVAL;
   }

return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],            NULL, prot) ? 0 : -1;
}


`inet_add_protocol`函数将tcp和udp对应的处理函数都注册到了inet\_protos数组中了。再看`dev_add_pack(&ip_packet_type);`这一行,ip\_packet\_type结构体中的type是协议名,func是ip\_rcv函数,在dev\_add\_pack中会被注册到ptype\_base哈希表中。



//file: net/core/dev.c
void dev_add_pack(struct packet_type *pt){
   struct list_head *head = ptype_head(pt);    …
}
static inline struct list_head *ptype_head(const struct packet_type *pt){
   if (pt->type == htons(ETH_P_ALL))
       return &ptype_all;
   else        return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}


这里我们需要记住inet\_protos记录着udp,tcp的处理函数地址,ptype\_base存储着ip\_rcv()函数的处理地址。后面我们会看到软中断中会通过ptype\_base找到ip\_rcv函数地址,进而将ip包正确地送到ip\_rcv()中执行。在ip\_rcv中将会通过inet\_protos找到tcp或者udp的处理函数,再而把包转发给udp\_rcv()或tcp\_v4\_rcv()函数。


扩展一下,如果看一下ip\_rcv和udp\_rcv等函数的代码能看到很多协议的处理过程。例如,ip\_rcv中会处理netfilter和iptable过滤,如果你有很多或者很复杂的 netfilter 或 iptables 规则,这些规则都是在软中断的上下文中执行的,会加大网络延迟。再例如,udp\_rcv中会判断socket接收队列是否满了。对应的相关内核参数是net.core.rmem\_max和net.core.rmem\_default。如果有兴趣,建议大家好好读一下`inet_init`这个函数的代码。


### 2.4 网卡驱动初始化


每一个驱动程序(不仅仅只是网卡驱动)会使用 module\_init 向内核注册一个初始化函数,当驱动被加载时,内核会调用这个函数。比如igb网卡驱动的代码位于`drivers/net/ethernet/intel/igb/igb_main.c`



//file: drivers/net/ethernet/intel/igb/igb_main.c
static struct pci_driver igb_driver = {
   .name     = igb_driver_name,
   .id_table = igb_pci_tbl,
   .probe    = igb_probe,
   .remove   = igb_remove,    …
};
static int __init igb_init_module(void){
   …
   ret = pci_register_driver(&igb_driver);    return ret;
}


驱动的`pci_register_driver`调用完成后,Linux内核就知道了该驱动的相关信息,比如igb网卡驱动的`igb_driver_name`和`igb_probe`函数地址等等。当网卡设备被识别以后,内核会调用其驱动的probe方法(igb\_driver的probe方法是igb\_probe)。驱动probe方法执行的目的就是让设备ready,对于igb网卡,其`igb_probe`位于drivers/net/ethernet/intel/igb/igb\_main.c下。主要执行的操作如下:


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWlpZQmpKaWFZWUJnR0hTb3A4VW9XWncwTW9JRXZTNm9EaWFpY1JPcWVyTHlCVkhwQ1RnYmRBcGNydy82NDA?x-oss-process=image/format,png)
 
 图6 网卡驱动初始化 
 
第5步中我们看到,网卡驱动实现了ethtool所需要的接口,也在这里注册完成函数地址的注册。当 ethtool 发起一个系统调用之后,内核会找到对应操作的回调函数。对于igb网卡来说,其实现函数都在drivers/net/ethernet/intel/igb/igb\_ethtool.c下。相信你这次能彻底理解ethtool的工作原理了吧?这个命令之所以能查看网卡收发包统计、能修改网卡自适应模式、能调整RX 队列的数量和大小,是因为ethtool命令最终调用到了网卡驱动的相应方法,而不是ethtool本身有这个超能力。


第6步注册的igb\_netdev\_ops中包含的是igb\_open等函数,该函数在网卡被启动的时候会被调用。



//file: drivers/net/ethernet/intel/igb/igb_main.c
static const struct net_device_ops igb_netdev_ops = {
 .ndo_open               = igb_open,
 .ndo_stop               = igb_close,
 .ndo_start_xmit         = igb_xmit_frame,
 .ndo_get_stats64        = igb_get_stats64,
 .ndo_set_rx_mode        = igb_set_rx_mode,
 .ndo_set_mac_address    = igb_set_mac,
 .ndo_change_mtu         = igb_change_mtu,  .ndo_do_ioctl           = igb_ioctl,
 …


第7步中,在igb\_probe初始化过程中,还调用到了`igb_alloc_q_vector`。他注册了一个NAPI机制所必须的poll函数,对于igb网卡驱动来说,这个函数就是igb\_poll,如下代码所示。



static int igb_alloc_q_vector(struct igb_adapter adapter,
                 int v_count, int v_idx,
                 int txr_count, int txr_idx,
                 int rxr_count, int rxr_idx){
   …
   /
initialize NAPI */
   netif_napi_add(adapter->netdev, &q_vector->napi,               igb_poll, 64);
}


### 2.5 启动网卡


当上面的初始化都完成以后,就可以启动网卡了。回忆前面网卡驱动初始化时,我们提到了驱动向内核注册了 structure net\_device\_ops 变量,它包含着网卡启用、发包、设置mac 地址等回调函数(函数指针)。当启用一个网卡时(例如,通过 ifconfig eth0 up),net\_device\_ops 中的 igb\_open方法会被调用。它通常会做以下事情:


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWlJWY0tPV2t2clBzZjRQZUNDU2JpYkZ4aWJ1b1hGbnRoT0l4YU0wSkxNTmlhOE1GYzlZeUFvUWd0dy82NDA?x-oss-process=image/format,png)
 
 图7 启动网卡 
 

//file: drivers/net/ethernet/intel/igb/igb_main.c
static int __igb_open(struct net_device netdev, bool resuming){
   /
allocate transmit descriptors */
   err = igb_setup_all_tx_resources(adapter);

/* allocate receive descriptors */
   err = igb_setup_all_rx_resources(adapter);

/* 注册中断处理函数 */
   err = igb_request_irq(adapter);
   if (err)
       goto err_req_irq;

/* 启用NAPI */
   for (i = 0; i < adapter->num_q_vectors; i++)
       napi_enable(&(adapter->q_vector[i]->napi));    …
}


在上面`__igb_open`函数调用了igb\_setup\_all\_tx\_resources,和igb\_setup\_all\_rx\_resources。在`igb_setup_all_rx_resources`这一步操作中,分配了RingBuffer,并建立内存和Rx队列的映射关系。(Rx Tx 队列的数量和大小可以通过 ethtool 进行配置)。我们再接着看中断函数注册`igb_request_irq`:



static int igb_request_irq(struct igb_adapter *adapter){
   if (adapter->msix_entries) {
       err = igb_request_msix(adapter);
       if (!err)
           goto request_done;
       …    }
}
static int igb_request_msix(struct igb_adapter *adapter){
   …
   for (i = 0; i < adapter->num_q_vectors; i++) {
       …
       err = request_irq(adapter->msix_entries[vector].vector,
                 igb_msix_ring, 0, q_vector->name,
   }


在上面的代码中跟踪函数调用, `__igb_open` => `igb_request_irq` => `igb_request_msix`, 在`igb_request_msix`中我们看到了,对于多队列的网卡,为每一个队列都注册了中断,其对应的中断处理函数是igb\_msix\_ring(该函数也在drivers/net/ethernet/intel/igb/igb\_main.c下)。我们也可以看到,msix方式下,每个 RX 队列有独立的MSI-X 中断,从网卡硬件中断的层面就可以设置让收到的包被不同的 CPU处理。(可以通过 irqbalance ,或者修改 /proc/irq/IRQ\_NUMBER/smp\_affinity能够修改和CPU的绑定行为)。


当做好以上准备工作以后,就可以开门迎客(数据包)了!


三


迎接数据的到来


### 3.1 硬中断处理


首先当数据帧从网线到达网卡上的时候,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU都是无感的。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWjNKSGdLajhHcFRaV0s2dkM0dWUySjh6Szh6RzNLMEZzaHBxaWJZMlozNjdoaWJ1RlFpYndZUXVFdy82NDA?x-oss-process=image/format,png)
 
 图8 网卡数据硬中断处理过程 
 

>  
>  注意:当RingBuffer满的时候,新来的数据包将给丢弃。ifconfig查看网卡的时候,可以里面有个overruns,表示因为环形队列满被丢弃的包。如果发现有丢包,可能需要通过ethtool命令来加大环形队列的长度。 
>  


在启动网卡一节,我们说到了网卡的硬中断注册的处理函数是igb\_msix\_ring。



//file: drivers/net/ethernet/intel/igb/igb_main.c
static irqreturn_t igb_msix_ring(int irq, void *data){
   struct igb_q_vector *q_vector = data;

/* Write the ITR value calculated from the previous interrupt. */
   igb_write_itr(q_vector);

napi_schedule(&q_vector->napi);    return IRQ_HANDLED;
}


`igb_write_itr`只是记录一下硬件中断频率(据说目的是在减少对CPU的中断频率时用到)。顺着napi\_schedule调用一路跟踪下去,`__napi_schedule`=>`____napi_schedule`



/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
                    struct napi_struct *napi){
   list_add_tail(&napi->poll_list, &sd->poll_list);    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}


这里我们看到,`list_add_tail`修改了CPU变量softnet\_data里的poll\_list,将驱动napi\_struct传过来的poll\_list添加了进来。其中softnet\_data中的poll\_list是一个双向列表,其中的设备都带有输入帧等着被处理。紧接着`__raise_softirq_irqoff`触发了一个软中断NET\_RX\_SOFTIRQ, 这个所谓的触发过程只是对一个变量进行了一次或运算而已。



void __raise_softirq_irqoff(unsigned int nr){
   trace_softirq_raise(nr);    or_softirq_pending(1UL << nr);
}
//file: include/linux/irq_cpustat.h
#define or_softirq_pending(x)  (local_softirq_pending() |= (x))


我们说过,Linux在硬中断里只完成简单必要的工作,剩下的大部分的处理都是转交给软中断的。通过上面代码可以看到,硬中断处理过程真的是非常短。只是记录了一个寄存器,修改了一下下CPU的poll\_list,然后发出个软中断。就这么简单,硬中断工作就算是完成了。


### 3.2 ksoftirqd内核线程处理软中断


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWlhhblNUdGdQWWVpYzZOVkZBUUhwTmZiVWlhRTg0ckwwVHF4ZlRDUmVVcmlhNlZkdmlhc2tOSllDbkEvNjQw?x-oss-process=image/format,png)
 
 图9 ksoftirqd内核线程 
 
内核线程初始化的时候,我们介绍了ksoftirqd中两个线程函数`ksoftirqd_should_run`和`run_ksoftirqd`。其中`ksoftirqd_should_run`代码如下:



static int ksoftirqd_should_run(unsigned int cpu){    return local_softirq_pending();
}
#define local_softirq_pending() \    __IRQ_STAT(smp_processor_id(), __softirq_pending)


这里看到和硬中断中调用了同一个函数`local_softirq_pending`。使用方式不同的是硬中断位置是为了写入标记,这里仅仅只是读取。如果硬中断中设置了`NET_RX_SOFTIRQ`,这里自然能读取的到。接下来会真正进入线程函数中`run_ksoftirqd`处理:



static void run_ksoftirqd(unsigned int cpu){
   local_irq_disable();
   if (local_softirq_pending()) {
       __do_softirq();
       rcu_note_context_switch(cpu);
       local_irq_enable();
       cond_resched();
       return;
   }    local_irq_enable();
}


在`__do_softirq`中,判断根据当前CPU的软中断类型,调用其注册的action方法。



asmlinkage void __do_softirq(void){
   do {
       if (pending & 1) {
           unsigned int vec_nr = h - softirq_vec;
           int prev_count = preempt_count();
           …
           trace_softirq_entry(vec_nr);
           h->action(h);
           trace_softirq_exit(vec_nr);
           …
       }
       h++;
       pending >>= 1;    } while (pending);
}


在网络子系统初始化小节, 我们看到我们为NET\_RX\_SOFTIRQ注册了处理函数net\_rx\_action。所以`net_rx_action`函数就会被执行到了。


这里需要注意一个细节,硬中断中设置软中断标记,和ksoftirq的判断是否有软中断到达,都是基于smp\_processor\_id()的。这意味着只要硬中断在哪个CPU上被响应,那么软中断也是在这个CPU上处理的。所以说,如果你发现你的Linux软中断CPU消耗都集中在一个核上的话,做法是要把调整硬中断的CPU亲和性,来将硬中断打散到不同的CPU核上去。


我们再来把精力集中到这个核心函数`net_rx_action`上来。



static void net_rx_action(struct softirq_action *h){
   struct softnet_data *sd = &__get_cpu_var(softnet_data);
   unsigned long time_limit = jiffies + 2;
   int budget = netdev_budget;
   void *have;

local_irq_disable();
   while (!list_empty(&sd->poll_list)) {
       …
       n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

work = 0;
       if (test_bit(NAPI_STATE_SCHED, &n->state)) {
           work = n->poll(n, weight);
           trace_napi_poll(n);
       }
       budget -= work;    }
}


函数开头的time\_limit和budget是用来控制net\_rx\_action函数主动退出的,目的是保证网络包的接收不霸占CPU不放。等下次网卡再有硬中断过来的时候再处理剩下的接收数据包。其中budget可以通过内核参数调整。这个函数中剩下的核心逻辑是获取到当前CPU变量softnet\_data,对其poll\_list进行遍历, 然后执行到网卡驱动注册到的poll函数。对于igb网卡来说,就是igb驱动力的`igb_poll`函数了。



static int igb_poll(struct napi_struct *napi, int budget){




   if (q_vector->tx.ring)
       clean_complete = igb_clean_tx_irq(q_vector);

if (q_vector->rx.ring)
       clean_complete &= igb_clean_rx_irq(q_vector, budget);    …
}


在读取操作中,`igb_poll`的重点工作是对`igb_clean_rx_irq`的调用。



static bool igb_clean_rx_irq(struct igb_q_vector q_vector, const int budget){
   …
   do {
       /
retrieve a buffer from the ring */
       skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);

/* fetch next buffer in frame if non-eop */
       if (igb_is_non_eop(rx_ring, rx_desc))
           continue;
       }

/* verify the packet layout is correct */
       if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {
           skb = NULL;
           continue;
       }

/* populate checksum, timestamp, VLAN, and protocol */
       igb_process_skb_fields(rx_ring, rx_desc, skb);

napi_gro_receive(&q_vector->napi, skb);



}


`igb_fetch_rx_buffer`和`igb_is_non_eop`的作用就是把数据帧从RingBuffer上取下来。为什么需要两个函数呢?因为有可能帧要占多多个RingBuffer,所以是在一个循环中获取的,直到帧尾部。获取下来的一个数据帧用一个sk\_buff来表示。收取完数据以后,对其进行一些校验,然后开始设置sbk变量的timestamp, VLAN id, protocol等字段。接下来进入到napi\_gro\_receive中:



//file: net/core/dev.c
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb){
   skb_gro_reset_offset(skb);    return napi_skb_finish(dev_gro_receive(napi, skb), skb);
}


`dev_gro_receive`这个函数代表的是网卡GRO特性,可以简单理解成把相关的小包合并成一个大包就行,目的是减少传送给网络栈的包数,这有助于减少 CPU 的使用量。我们暂且忽略,直接看`napi_skb_finish`, 这个函数主要就是调用了`netif_receive_skb`。



//file: net/core/dev.c
static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb){
   switch (ret) {
   case GRO_NORMAL:
       if (netif_receive_skb(skb))
           ret = GRO_DROP;
       break;    …
}


在`netif_receive_skb`中,数据包将被送到协议栈中。声明,以下的3.3, 3.4, 3.5也都属于软中断的处理过程,只不过由于篇幅太长,单独拿出来成小节。


### 3.3 网络协议栈处理


`netif_receive_skb`函数会根据包的协议,假如是udp包,会将包依次送到ip\_rcv(),udp\_rcv()协议处理函数中进行处理。


![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9CQmpBRkY0aGN3cHVscFZTU09aelYzRGtob0lrMHFrWnR5UzVlaWFlbUEyNTJ5OWp4OEJBNmFBQnlDbmJsWTBwYkl4T2JsV2dCUjBxbUFJelRDTWg5YmcvNjQw?x-oss-process=image/format,png)
 
 图10 网络协议栈处理 
 

//file: net/core/dev.c
int netif_receive_skb(struct sk_buff *skb){
   //RPS处理逻辑,先忽略    …    return __netif_receive_skb(skb);
}
static int __netif_receive_skb(struct sk_buff *skb){
   …  
   ret = __netif_receive_skb_core(skb, false);}static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc){
   …

//pcap逻辑,这里会将数据送入抓包点。tcpdump就是从这个入口获取包的    list_for_each_entry_rcu(ptype, &ptype_all, list) {
       if (!ptype->dev || ptype->dev == skb->dev) {
           if (pt_prev)
               ret = deliver_skb(skb, pt_prev, orig_dev);
           pt_prev = ptype;
       }
   }
   …
   list_for_each_entry_rcu(ptype,
           &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
       if (ptype->type == type &&
           (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
            ptype->dev == orig_dev)) {
           if (pt_prev)
               ret = deliver_skb(skb, pt_prev, orig_dev);
           pt_prev = ptype;
       }    }
}


在`__netif_receive_skb_core`中,我看着原来经常使用的tcpdump的抓包点,很是激动,看来读一遍源代码时间真的没白浪费。接着`__netif_receive_skb_core`取出protocol,它会从数据包中取出协议信息,然后遍历注册在这个协议上的回调函数列表。`ptype_base` 是一个 hash table,在协议注册小节我们提到过。ip\_rcv 函数地址就是存在这个 hash table中的。



//file: net/core/dev.c
static inline int deliver_skb(struct sk_buff *skb,
                 struct packet_type *pt_prev,
                 struct net_device *orig_dev){
   …    return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}


`pt_prev->func`这一行就调用到了协议层注册的处理函数了。对于ip包来讲,就会进入到`ip_rcv`(如果是arp包的话,会进入到arp\_rcv)。


### 3.4 IP协议层处理


我们再来大致看一下linux在ip协议层都做了什么,包又是怎么样进一步被送到udp或tcp协议处理函数中的。



//file: net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){
   …
   return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,               ip_rcv_finish);
}


这里`NF_HOOK`是一个钩子函数,当执行完注册的钩子后就会执行到最后一个参数指向的函数`ip_rcv_finish`。



static int ip_rcv_finish(struct sk_buff *skb){
   …
   if (!skb_dst(skb)) {
       int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
                          iph->tos, skb->dev);
       …
   }
   …    return dst_input(skb);
}


跟踪`ip_route_input_noref` 后看到它又调用了 `ip_route_input_mc`。在`ip_route_input_mc`中,函数`ip_local_deliver`被赋值给了`dst.input`, 如下:



//file: net/ipv4/route.c
static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,u8 tos, struct net_device *dev, int our){
   if (our) {
       rth->dst.input= ip_local_deliver;
       rth->rt_flags |= RTCF_LOCAL;    }
}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Linux运维工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)
img

rt_flags |= RTCF_LOCAL;    }
}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Linux运维工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-0Tt3sejr-1712884409039)]
[外链图片转存中…(img-1mRylLzO-1712884409040)]
[外链图片转存中…(img-7WyxJRKs-1712884409041)]
[外链图片转存中…(img-n5ZD0v5U-1712884409041)]
[外链图片转存中…(img-eYAyhCou-1712884409041)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)
[外链图片转存中…(img-jxEcPDZb-1712884409042)]

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值