目录:
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要做许多的筹备工作:
- 创立ksoftirqd线程,为它设置好它本人的线程函数,前面就指望着它来解决软中断呢。
- 协定栈注册,linux要实现许多协定,比方arp,icmp,ip,udp,tcp,每一个协定都会将本人的处理函数注册一下,不便包来了迅速找到对应的处理函数
- 网卡驱动初始化,每个驱动都有一个初始化函数,内核会让驱动也初始化一下。在这个初始化过程中,把本人的DMA筹备好,把NAPI的poll函数地址通知内核
- 启动网卡,调配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 中断上半文
物理网卡收到数据包的解决流程如上图左半局部所示,具体步骤如下:
- 网卡收到数据包,先将高低电平转换到网卡fifo存储,网卡申请ring buffer的形容,依据形容找到具体的物理地址,从fifo队列物理网卡会应用DMA将数据包写到了该物理地址,,其实就是skb_buffer中.
- 这个时候数据包曾经被转移到skb_buffer中,因为是DMA写入,内核并没有监控数据包写入状况,这时候NIC触发一个硬中断,每一个硬件中断会对应一个中断号,且指定一个vCPU来解决,如上图vcpu2收到了该硬件中断.
- 硬件中断的中断处理程序,调用驱动程序实现,a.启动软中断
- 硬中断触发的驱动程序会禁用网卡硬中断,其实这时候意思是通知NIC,再来数据不必触发硬中断了,把数据DMA拷入零碎内存即可
- 硬中断触发的驱动程序会启动软中断,启用软中断目标是将数据包后续解决流程交给软中断缓缓解决,这个时候退出硬件中断了,然而留神和网络无关的硬中断,要等到后续开启硬中断后,才有机会再次被触发
- NAPI触发软中断,触发napi零碎
- 耗费ringbuffer指向的skb_buffer
- NAPI循环解决ringbuffer数据,解决实现
- 启动网络硬件中断,有数据来时候就能够持续触发硬件中断,持续告诉CPU来耗费数据包.
其实上述过程过程简略形容为:网卡收到数据包,DMA到内核内存,中断告诉内核数据有了,内核按轮次解决耗费数据包,一轮解决实现后,开启硬中断。其外围就是网卡和内核其实是生产和生产模型,网卡生产,内核负责生产,生产者须要告诉消费者生产;如果生产过快会产生丢包,如果生产过慢也会产生问题。也就说在高流量压力状况下,只有生产生产优化后,生产能力够快,此生产生产关系才能够失常维持,所以如果物理接口有丢包计数时候,未必是网卡存在问题,也可能是内核生产的太慢。
对于CPU与ksoftirqd的关系能够形容如下:
l 网卡收到的数据写入到内核内存
NIC在接管到数据包之后,首先须要将数据同步到内核中,这两头的桥梁是rx ring buffer。它是由NIC和驱动程序共享的一片区域,事实上,rx ring buffer存储的并不是理论的packet数据,而是一个描述符,这个描述符指向了它真正的存储地址,具体流程如下:
- 驱动在内存中调配一片缓冲区用来接管数据包,叫做sk_buffer;
- 将上述缓冲区的地址和大小(即接管描述符),退出到rx ring buffer。描述符中的缓冲区地址是DMA应用的物理地址;
- 驱动告诉网卡有一个新的描述符;
- 网卡从rx ring buffer中取出描述符,从而获知缓冲区的地址和大小;
- 网卡收到新的数据包;
- 网卡将新数据包通过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构造,而后用其必须的例程进行初始化。