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

目录:

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)的差异,因而采用分层结构:
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第1张图片

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

相关视频推荐
手把手带你实现一个Linux内核文件系统
Linux内核文件系统具体实现与内核裁
TCP/IP协议栈深度解析丨实现单机百万连接丨优化三次握手、四次挥手丨优化TCP的传输速率
LinuxC++后台服务器开发免费学习:C/C++Linux服务器开发/后台架构师-学习视频

TCP/IP分层模型

在TCP/IP网络分层模型里,整个协议栈被分成了物理层、链路层、网络层,传输层和应用层。物理层对应的是网卡和网线,应用层对应的是我们常见的Nginx,FTP等等各种应用。Linux实现的是链路层、网络层和传输层这三层。
在Linux内核实现中,链路层协议靠网卡驱动来实现,内核协议栈来实现网络层和传输层。内核对更上层的应用层提供socket接口来供用户进程访问。我们用Linux的视角来看到的TCP/IP网络分层模型应该是下面这个样子的。
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第2张图片

首先我们梳理一下每层模型的职责:
链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据;
网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发;
传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序;
应用层:定义数据格式,并按照对应的格式解读数据。

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

Linux 网络协议栈

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第3张图片

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

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第4张图片
以上说明基本大致说明了TCP/IP中TCP,UDP协议包在网络子系统中的实现流程。本文主要在链路层中,即关于网卡收报触发中断到进入网络层之间的过程探究。

【文章福利】小编推荐自己的linuxC/C++语言交流群:832218493,整理了一些个人觉得比较好的学习书籍、视频资料共享在里面,有需要的可以自行添加哦!~!
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第5张图片

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;

}

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第6张图片
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第7张图片

从硬中断到软中断
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第8张图片

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进程。

image.png

相关代码如下:
//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",

};

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第9张图片
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第10张图片

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

l 创建ksoftirqd内核线程

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第11张图片

linux内核通过调用subsys_initcall来初始化各个子系统,在源代码目录里你可以grep出许多对这个函数的调用。这里我们要说的是网络子系统的初始化,会执行到net_dev_init函数。

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第12张图片
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第13张图片
image.png

在这个函数里,会为每个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.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,

};

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第14张图片
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第15张图片
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第16张图片

扩展一下,如果看一下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下。主要执行的操作如下:
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第17张图片

第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方法会被调用。它通常会做以下事情:
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第18张图片

//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模型中属于物理层和链路层,如下图所示。

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第19张图片
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第20张图片

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的关系可以描述如下:

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第21张图片

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中。

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第22张图片

当驱动处理速度跟不上网卡收包速度时,驱动来不及分配缓冲区,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上送协议栈。

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第23张图片

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

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第24张图片

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)
结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第25张图片

在网络子系统初始化小节,我们看到我们为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上送协议栈

结合中断分析TCP/IP协议栈在LINUX内核中的运行时序_第26张图片

Linux网络子系统的分层
Linux网络子系统实现需要:
l 支持不同的协议族 ( INET, INET6, UNIX, NETLINK...)
l 支持不同的网络设备
l 支持统一的BSD socket API
需要屏蔽协议、硬件、平台(API)的差异,因而采用分层结构:

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

你可能感兴趣的