关于tcp-ip:结合中断分析TCPIP协议栈在LINUX内核中的运行时序

37次阅读

共计 15999 个字符,预计需要花费 40 分钟才能阅读完成。

目录:

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 构造,而后用其必须的例程进行初始化。

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

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 协定包在网络子系统中的实现流程。本文次要在链路层中,即对于网卡收报触发中断到进入网络层之间的过程探索。

【文章福利】小编举荐本人的 linuxC/C++ 语言交换群:832218493,整顿了一些集体感觉比拟好的学习书籍、视频材料共享在外面,有须要的能够自行添加哦!~!

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.c

static 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.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,

};



扩大一下,如果看一下 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 构造,而后用其必须的例程进行初始化。

正文完
 0