目录:

1.Linux网络子系统的分层

2.TCP/IP分层模型

3.Linux 网络协议栈

4.Linux 网卡收包时的中断处理问题

5.Linux 网络启动的准备工作

6.Linux网络包:中断到网络层接收

7.总结

Linux网络子系统的分层

Linux网络子系统实现需要:

l  支持不同的协议族 ( INET, INET6, UNIX, NETLINK...)

l  支持不同的网络设备

l  支持统一的BSD socket API

需要屏蔽协议、硬件、平台(API)的差异,因而采用分层结构:

系统调用提供用户的应用程序访问内核的唯一途径。协议无关接口由socket layer来实现的,其提供一组通用功能,以支持各种不同的协议。网络协议层为socket层提供具体协议接口——proto{},实现具体的协议细节。设备无关接口,提供一组通用函数供底层网络设备驱动程序使用。设备驱动与特定网卡设备相关,定义了具体的协议细节,会分配一个net_device结构,然后用其必需的例程进行初始化。

TCP/IP分层模型

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

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

首先我们梳理一下每层模型的职责:

链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据;

网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发;

传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序;

应用层:定义数据格式,并按照对应的格式解读数据。

然后再把每层模型的职责串联起来,用一句通俗易懂的话讲就是:

当你输入一个网址并按下回车键的时候,首先,应用层协议对该请求包做了格式定义;紧接着传输层协议加上了双方的端口号,确认了双方通信的应用程序;然后网络协议加上了双方的IP地址,确认了双方的网络位置;最后链路层协议加上了双方的MAC地址,确认了双方的物理位置,同时将数据进行分组,形成数据帧,采用广播方式,通过传输介质发送给对方主机。而对于不同网段,该数据包首先会转发给网关路由器,经过多次转发后,最终被发送到目标主机。目标机接收到数据包后,采用对应的协议,对帧数据进行组装,然后再通过一层一层的协议进行解析,最终被应用层的协议解析并交给服务器处理。

Linux 网络协议栈

基于TCP/IP协议栈的send/recv在应用层,传输层,网络层和链路层中具体函数调用过程已经有很多人研究,本文引用一张比较完善的图如下:

以上说明基本大致说明了TCP/IP中TCP,UDP协议包在网络子系统中的实现流程。本文主要在链路层中,即关于网卡收报触发中断到进入网络层之间的过程探究。

Linux 网卡收包时的中断处理问题

中断,一般指硬件中断,多由系统自身或与之链接的外设(如键盘、鼠标、网卡等)产生。中断首先是处理器提供的一种响应外设请求的机制,是处理器硬件支持的特性。一个外设通过产生一种电信号通知中断控制器,中断控制器再向处理器发送相应的信号。处理器检测到了这个信号后就会打断自己当前正在做的工作,转而去处理这次中断(所以才叫中断)。当然在转去处理中断和中断返回时都有保护现场和返回现场的操作,这里不赘述。

那软中断又是什么呢?我们知道在中断处理时CPU没法处理其它事物,对于网卡来说,如果每次网卡收包时中断的时间都过长,那很可能造成丢包的可能性。当然我们不能完全避免丢包的可能性,以太包的传输是没有100%保证的,所以网络才有协议栈,通过高层的协议来保证连续数据传输的数据完整性(比如在协议发现丢包时要求重传)。但是即使有协议保证,那我们也不能肆无忌惮的使用中断,中断的时间越短越好,尽快放开处理器,让它可以去响应下次中断甚至进行调度工作。基于这样的考虑,我们将中断分成了上下两部分,上半部分就是上面说的中断部分,需要快速及时响应,同时需要越快结束越好。而下半部分就是完成一些可以推后执行的工作。对于网卡收包来说,网卡收到数据包,通知内核数据包到了,中断处理将数据包存入内存这些都是急切需要完成的工作,放到上半部完成。而解析处理数据包的工作则可以放到下半部去执行。

软中断就是下半部使用的一种机制,它通过软件模仿硬件中断的处理过程,但是和硬件没有关系,单纯的通过软件达到一种异步处理的方式。其它下半部的处理机制还包括tasklet,工作队列等。依据所处理的场合不同,选择不同的机制,网卡收包一般使用软中断。对应NET_RX_SOFTIRQ这个软中,软中断的类型如下:

enum
{HI_SOFTIRQ=0,TIMER_SOFTIRQ,NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,IRQ_POLL_SOFTIRQ,TASKLET_SOFTIRQ,SCHED_SOFTIRQ,HRTIMER_SOFTIRQ,RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */NR_SOFTIRQS
};

通过以上可以了解到,Linux中断注册显然应该包括网卡的硬中断,包处理的软中断两个步骤。

l  注册网卡中断

我们以一个具体的网卡驱动为例,比如e1000。其模块初始化函数就是:

static int __init e1000_init_module(void)
{int ret;pr_info("%s - version %s\n", e1000_driver_string, e1000_driver_version);pr_info("%s\n", e1000_copyright);ret = pci_register_driver(&e1000_driver);
...return ret;}

其中e1000_driver这个结构体是一个关键,这个结构体中很主要的一个方法就是.probe方法,也就是e1000_probe():

/**                                                  * e1000_probe - Device Initialization Routine         * @pdev: PCI device information struct                    * @ent: entry in e1000_pci_tbl     *                                * Returns 0 on success, negative on failure                                                                               *                                                                                                               * e1000_probe initializes an adapter identified by a pci_dev structure.                                                               * The OS initialization, configuring of the adapter private structure,                                                                  * and a hardware reset occur.                                                      **/
static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
...
...netdev->netdev_ops = &e1000_netdev_ops;e1000_set_ethtool_ops(netdev);
...
...
}

这个函数很长,我们不都列出来,这是e1000主要的初始化函数,即使从注释都能看出来。我们留意其注册了netdev的netdev_ops,用的是e1000_netdev_ops这个结构体:

static const struct net_device_ops e1000_netdev_ops = {.ndo_open               = e1000_open,.ndo_stop               = e1000_close,.ndo_start_xmit         = e1000_xmit_frame,.ndo_set_rx_mode        = e1000_set_rx_mode,.ndo_set_mac_address    = e1000_set_mac,.ndo_tx_timeout         = e1000_tx_timeout,
...
...
}; 

这个e1000的方法集里有一个重要的方法,e1000_open,我们要说的中断的注册就从这里开始:

/**           * e1000_open - Called when a network interface is made active  * @netdev: network interface device structure            *                                                 * Returns 0 on success, negative value on failure     *     * The open entry point is called when a network interface is made                                                                                                    * active by the system (IFF_UP).  At this point all resources needed                                                                            * for transmit and receive operations are allocated, the interrupt                                                     * handler is registered with the OS, the watchdog task is started,                                                                                                     * and the stack is notified that the interface is ready.                                                                                                             **/
int e1000_open(struct net_device *netdev)
{struct e1000_adapter *adapter = netdev_priv(netdev);struct e1000_hw *hw = &adapter->hw;
...
...err = e1000_request_irq(adapter);
...
} 

e1000在这里注册了中断:

static int e1000_request_irq(struct e1000_adapter *adapter)
{struct net_device *netdev = adapter->netdev;irq_handler_t handler = e1000_intr;int irq_flags = IRQF_SHARED;int err;err = request_irq(adapter->pdev->irq, handler, irq_flags, netdev->name,
...
...
} 

如上所示,这个被注册的中断处理函数,也就是handler,就是e1000_intr()。我们不展开这个中断处理函数看了,我们知道中断处理函数在这里被注册了,在网络包来的时候会触发这个中断函数。

l  注册软中断

内核初始化期间,softirq_init会注册TASKLET_SOFTIRQ以及HI_SOFTIRQ相关联的处理函数。

void __init softirq_init(void)
{......open_softirq(TASKLET_SOFTIRQ, tasklet_action);open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

网络子系统分两种soft IRQ。NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,分别处理发送数据包和接收数据包。这两个soft IRQ在net_dev_init函数(net/core/dev.c)中注册:

open_softirq(NET_TX_SOFTIRQ, net_tx_action);

open_softirq(NET_RX_SOFTIRQ, net_rx_action);

收发数据包的软中断处理函数被注册为net_rx_action和net_tx_action。其中open_softirq实现为:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{softirq_vec[nr].action = action;
}

  • 从硬中断到软中断

Linux 网络启动的准备工作

首先在开始收包之前,Linux要做许多的准备工作:

1. 创建ksoftirqd线程,为它设置好它自己的线程函数,后面就指望着它来处理软中断呢。

2. 协议栈注册,linux要实现许多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数注册一下,方便包来了迅速找到对应的处理函数

3. 网卡驱动初始化,每个驱动都有一个初始化函数,内核会让驱动也初始化一下。在这个初始化过程中,把自己的DMA准备好,把NAPI的poll函数地址告诉内核

4. 启动网卡,分配RX,TX队列,注册中断对应的处理函数

l  创建ksoftirqd内核线程

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

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

相关代码如下:

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

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

l  创建ksoftirqd内核线程

linux内核通过调用subsys_initcall来初始化各个子系统,在源代码目录里你可以grep出许多对这个函数的调用。这里我们要说的是网络子系统的初始化,会执行到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线程收到软中断的时候,也会使用这个变量来找到每一种软中断对应的处理函数。

l  协议栈注册

内核实现了网络层的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数据结构中

相关代码如下

//file: net/ipv4/af_inet.cstatic 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,
};

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

l  网卡驱动初始化

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

驱动的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下。主要执行的操作如下:

第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.
......
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);
} 

l  启动网卡

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

//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的绑定行为)。

到此准备工作完成。

Linux网络包:中断到网络层接收

网卡收包从整体上是网线中的高低电平转换到网卡FIFO存储再拷贝到系统主内存(DDR3)的过程,其中涉及到网卡控制器,CPU,DMA,驱动程序,在OSI模型中属于物理层和链路层,如下图所示。

l  中断上半文

物理网卡收到数据包的处理流程如上图左半部分所示,详细步骤如下:

1. 网卡收到数据包,先将高低电平转换到网卡fifo存储,网卡申请ring buffer的描述,根据描述找到具体的物理地址,从fifo队列物理网卡会使用DMA将数据包写到了该物理地址,,其实就是skb_buffer中.

2. 这个时候数据包已经被转移到skb_buffer中,因为是DMA写入,内核并没有监控数据包写入情况,这时候NIC触发一个硬中断,每一个硬件中断会对应一个中断号,且指定一个vCPU来处理,如上图vcpu2收到了该硬件中断.

3. 硬件中断的中断处理程序,调用驱动程序完成,a.启动软中断

4. 硬中断触发的驱动程序会禁用网卡硬中断,其实这时候意思是告诉NIC,再来数据不用触发硬中断了,把数据DMA拷入系统内存即可

5. 硬中断触发的驱动程序会启动软中断,启用软中断目的是将数据包后续处理流程交给软中断慢慢处理,这个时候退出硬件中断了,但是注意和网络有关的硬中断,要等到后续开启硬中断后,才有机会再次被触发

6. NAPI触发软中断,触发napi系统

7. 消耗ringbuffer指向的skb_buffer

8. NAPI循环处理ringbuffer数据,处理完成

9. 启动网络硬件中断,有数据来时候就可以继续触发硬件中断,继续通知CPU来消耗数据包.

其实上述过程过程简单描述为:网卡收到数据包,DMA到内核内存,中断通知内核数据有了,内核按轮次处理消耗数据包,一轮处理完成后,开启硬中断。其核心就是网卡和内核其实是生产和消费模型,网卡生产,内核负责消费,生产者需要通知消费者消费;如果生产过快会产生丢包,如果消费过慢也会产生问题。也就说在高流量压力情况下,只有生产消费优化后,消费能力够快,此生产消费关系才可以正常维持,所以如果物理接口有丢包计数时候,未必是网卡存在问题,也可能是内核消费的太慢。

关于CPU与ksoftirqd的关系可以描述如下:

l  网卡收到的数据写入到内核内存

NIC在接收到数据包之后,首先需要将数据同步到内核中,这中间的桥梁是rx ring buffer。它是由NIC和驱动程序共享的一片区域,事实上,rx ring buffer存储的并不是实际的packet数据,而是一个描述符,这个描述符指向了它真正的存储地址,具体流程如下:

1. 驱动在内存中分配一片缓冲区用来接收数据包,叫做sk_buffer;

2. 将上述缓冲区的地址和大小(即接收描述符),加入到rx ring buffer。描述符中的缓冲区地址是DMA使用的物理地址;

3. 驱动通知网卡有一个新的描述符;

4. 网卡从rx ring buffer中取出描述符,从而获知缓冲区的地址和大小;

5. 网卡收到新的数据包;

6. 网卡将新数据包通过DMA直接写到sk_buffer中。

当驱动处理速度跟不上网卡收包速度时,驱动来不及分配缓冲区,NIC接收到的数据包无法及时写到sk_buffer,就会产生堆积,当NIC内部缓冲区写满后,就会丢弃部分数据,引起丢包。这部分丢包为rx_fifo_errors,在 /proc/net/dev中体现为fifo字段增长,在ifconfig中体现为overruns指标增长。

l  中断下半文

ksoftirqd内核线程处理软中断,即中断下半部分软中断处理过程:

1.NAPI(以e1000网卡为例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()

2.非NAPI(以dm9000网卡为例):net_rx_action() -> process_backlog() -> netif_receive_skb()

最后网卡驱动通过netif_receive_skb()将sk_buff上送协议栈。

内核线程初始化的时候,我们介绍了ksoftirqd中两个线程函数ksoftirqd_should_run和run_ksoftirqd。其中ksoftirqd_should_run代码如下:

#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)

在网络子系统初始化小节,我们看到我们为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函数了。

/***  igb_poll - NAPI Rx polling callback*  @napi: napi polling structure*  @budget: count of how many packets we should handle**/
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中,数据包将被送到协议栈中,接下来在网络层协议层的处理流程便不再赘述。

总结

l  send发包过程

1、网卡驱动创建tx descriptor ring(一致性DMA内存),将tx descriptor ring的总线地址写入网卡寄存器TDBA

2、协议栈通过dev_queue_xmit()将sk_buff下送网卡驱动

3、网卡驱动将sk_buff放入tx descriptor ring,更新TDT

4、DMA感知到TDT的改变后,找到tx descriptor ring中下一个将要使用的descriptor

5、DMA通过PCI总线将descriptor的数据缓存区复制到Tx FIFO

6、复制完后,通过MAC芯片将数据包发送出去

7、发送完后,网卡更新TDH,启动硬中断通知CPU释放数据缓存区中的数据包

l  recv收包过程

1、网卡驱动创建rx descriptor ring(一致性DMA内存),将rx descriptor ring的总线地址写入网卡寄存器RDBA

2、网卡驱动为每个descriptor分配sk_buff和数据缓存区,流式DMA映射数据缓存区,将数据缓存区的总线地址保存到descriptor

3、网卡接收数据包,将数据包写入Rx FIFO

4、DMA找到rx descriptor ring中下一个将要使用的descriptor

5、整个数据包写入Rx FIFO后,DMA通过PCI总线将Rx FIFO中的数据包复制到descriptor的数据缓存区

6、复制完后,网卡启动硬中断通知CPU数据缓存区中已经有新的数据包了,CPU执行硬中断函数:

NAPI(以e1000网卡为例):e1000_intr() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)

非NAPI(以dm9000网卡为例):dm9000_interrupt() -> dm9000_rx() -> netif_rx() -> napi_schedule() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)

7、ksoftirqd执行软中断函数net_rx_action():

NAPI(以e1000网卡为例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()

非NAPI(以dm9000网卡为例):net_rx_action() -> process_backlog() -> netif_receive_skb()

8、网卡驱动通过netif_receive_skb()将sk_buff上送协议栈

Linux网络子系统的分层

Linux网络子系统实现需要:

l  支持不同的协议族 ( INET, INET6, UNIX, NETLINK...)

l  支持不同的网络设备

l  支持统一的BSD socket API

需要屏蔽协议、硬件、平台(API)的差异,因而采用分层结构:

系统调用提供用户的应用程序访问内核的唯一途径。协议无关接口由socket layer来实现的,其提供一组通用功能,以支持各种不同的协议。网络协议层为socket层提供具体协议接口——proto{},实现具体的协议细节。设备无关接口,提供一组通用函数供底层网络设备驱动程序使用。设备驱动与特定网卡设备相关,定义了具体的协议细节,会分配一个net_device结构,然后用其必需的例程进行初始化。

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序相关推荐

  1. TCP/IP协议栈在Linux内核中的运行时序分析

    本文主要是讲解TCP/IP协议栈在Linux内核中的运行时序,文章较长,里面有配套的视频讲解,建议收藏观看. 1 Linux概述 1.1 Linux操作系统架构简介 Linux操作系统总体上由Linu ...

  2. linux下IPROTO_TCP,TCP/IP协议栈在Linux内核中的运行时序分析

    可选题目三:TCP/IP协议栈在Linux内核中的运行时序分析 在深入理解Linux内核任务调度(中断处理.softirg.tasklet.wq.内核线程等)机制的基础上,分析梳理send和recv过 ...

  3. TCP/IP协议栈在Linux内核中的运行时序分析【万字长文】

    1 Linux概述 1.1 Linux操作系统架构简介 Linux操作系统总体上由Linux内核和GNU系统构成,具体来讲由4个主要部分构成,即Linux内核.Shell.文件系统和应用程序.内核.S ...

  4. TCP/IP网络协议栈在Linux内核中的如何使用丨内核开发丨驱动开发丨操作系统丨内核源码

    TCP/IP网络协议栈在Linux内核中的如何使用 视频讲解如下,点击观看: TCP/IP网络协议栈在Linux内核中的如何使用丨内核开发丨驱动开发丨操作系统丨内核源码 C/C++Linux服务器开发 ...

  5. TCP/IP协议栈到底是内核态好还是用户态好?

    "TCP/IP协议栈到底是内核态的好还是用户态的好?" 问题的根源在于,干嘛非要这么刻意地去区分什么内核态和用户态. 引子 为了不让本文成为干巴巴的说教,在文章开头,我以一个实例分 ...

  6. TCP/IP协议栈到底是内核态的好还是用户态的好?

    "TCP/IP协议栈到底是内核态的好还是用户态的好?"这根本就是一个错误的问题,问题的根源在于,干嘛非要这么刻意地去区分什么内核态和用户态. 引子 为了不让本文成为干巴巴的说教,在 ...

  7. 源码公开的TCP/IP协议栈在远程监测中的应用

    目前,随着互联网的发展,越来越多的工业测控设备已经将网络接入功能作为其默认配置,以实现设备的远程监控和信息分布式处理.笔者曾参与某发电机射频监测仪的开发,该设备主要用于诊断和预警发电机早期故障,并通过 ...

  8. Linux内核TCP/IP协议栈运行时序 | 配图

    TCP/IP协议栈在Linux内核中的运行时序分析[万字长文]

  9. linux 协议栈之socket,Linux TCP/IP 协议栈之 Socket 的实现分析(一)

    内核版本:2.6.37 参考[作者:kendo的文章(基于内涵版本2.6.12)] 第一部份 Socket套接字的创建 socket 并不是 TCP/IP协议的一部份. 从广义上来讲,socket 是 ...

  10. TCP/IP协议栈之LwIP(四)---网络诊断与状态查询(ICMPv4 + ICMPv6)

    文章目录 一.ICMP协议简介 1.1 ICMPv4报文功能 1.2 ICMPv6报文功能 二.PC常用网络命令 三.ICMP协议实现 3.1 ICMPv4数据报描述 3.2 ICMPv4数据报操作函 ...

最新文章

  1. 监控神器Prometheus用不对,也就是把新手村的剑
  2. GitHub star 7k+:史上最全图像分割资料集锦
  3. Flash使用域内存api的MD5,速度超快!
  4. UTF-8格式的文本文件程序读取异常
  5. 结对编程 - 马尔科夫链
  6. C语言课后习题(5)
  7. JAVA Swing GUI设计 WindowBuilder Pro Container使用大全6——JLayeredPane使用
  8. dr. unarchiver_1F 新店开业男士一生仅能定制一枚的钻戒,DR 教你用一生爱一人
  9. 不同手机型号图文预览_手机支持型号汇总
  10. MySQL 查询各科前三名(考虑成绩并列情况)
  11. 【爬虫】【原创】08 使用简单正则表达式爬取下厨房(早餐,午餐,晚餐)
  12. 5G NR 调度、帧结构相关
  13. 基于 Python 的地理空间绘图指南
  14. 如何看Mac电脑的具体CPU型号
  15. vlog 12110错误及解决
  16. 浅析image,imagesc,imshow的用法
  17. Android学习笔记-recreate()方法导致fragment重复新建的问题,解决方法
  18. STM32F103ZET6代码移植到F103C8T6上的方法及注意事项
  19. I4mc-deep: 利用具有化学特性的深度学习方法,对 n4- 甲基胞嘧啶位点进行智能预测
  20. maatwebsite/excel^3.1 excel中文环境不能完美设置列宽的问题

热门文章

  1. 1.swt/rap学习源码网址
  2. IE、Firefox和 Chrome长时间打开后内存都会变很大。
  3. 迪杰斯特拉算法c语言实现
  4. 给iphone或touch加桌面图标
  5. KVO.非常简单的键值监听模式
  6. Android Hessian 通信
  7. java教程:十分钟理解Java中的弱引用
  8. 猴子数据分享微信域名防封技术
  9. 电商系统设计之商品接口
  10. socket简介 - 获取简单网页内容