购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.3 内核接收报文流程

本节讨论Linux操作系统中网络收包的完整流程,即从硬件收到报文开始,到硬件驱动,再到内核协议栈,最后送到应用程序。

图2-7展示了一个IP报文从硬件设备到应用程序的总体流程。

图2-7 内核接收报文的总体流程

2.3.1 硬件设备接收报文

在Linux系统中,内核可以通过如下两种方式从设备中收取报文。

轮询: 内核定时查询设备状态,例如,创建内核线程定时查询某个设备的寄存器,以便知道该设备是否有报文到达。一般认为轮询方式比较浪费资源,因为绝大多数查询操作都是无效的。但是,这并不能说明轮询方式毫无作用,在一些高速转发场景下,轮询方式反而有优势。

中断: 设备接收到外部报文后,通过中断信号告知CPU该设备产生了一个事件,CPU会暂停目前执行的任务,转而调用中断处理函数处理此硬件事件。这种方案能够让系统对外部事件做出最及时的响应,但是也有代价,即如果中断频率过高,会导致系统频繁切换,降低性能。

按照中断源不同,中断可分为硬件中断与软件中断。

硬件中断(Hardware Interrupt): 硬件设备连接到CPU的中断信号线上,当设备产生中断时,在中断信号线上触发电平跳变,CPU捕获此电平跳变后,认为设备产生中断事件,立即开始执行中断处理程序。

软件中断(Software Interrupt): 由特定指令产生的中断,例如,在x86体系架构下,通过“int xx”指令可产生软件中断。Linux的系统调用功能正是通过软件中断实现的。

不管是硬件中断还是软件中断,每个中断都有自己的中断处理程序,一旦中断触发,CPU都会立即得到响应。

软件中断和软中断

提到了软件中断(Software Interrupt)就不得不提Linux内核的软中断(Soft IRQ),两者的中文翻译一字之差,实际上一点关系都没有。

软件中断属于中断的一种,中断触发后CPU立即响应,等价于硬件操作。而内核的软中断仅仅是一种软件实现,内核为每个CPU创建一个ksoftirqd软件线程,该线程守护本CPU上发生的软中断事件,当有软中断事件发生时,ksoftirqd线程执行此事件对应的处理函数。既然软中断是由软件线程实现的,那么必然存在调度,所以其响应时间相较于软件中断来说并不特别及时。另外,在使用软中断时,如果仅仅设置了软中断标志而不触发ksoftirqd线程调度,那么软中断也不会得到执行。

现在的网络设备一般同时支持中断、轮询两种模式,两种模式差异较大,其应用场景也完全不同。对于网络应用来说,当网络流量比较低的时候,中断是占绝对优势的,但是一旦流量达到一定速率(如超过10Gbps),继续采用中断模式会导致应用程序在内核态的处理时间过长,网络处理效率降低。而此时如果采用轮询机制,反而能提高系统性能。目前业界熟知的Intel开源的DPDK(Data Plane Development Kit,数据平面开发套件)网络加速库正是基于轮询机制而实现的。

我们需要重点关注的是Linux系统对网络报文处理的通用过程,下面将以中断方式为例讲述Linux接收报文的流程。

当网络硬件设备收到有效的数据报文后,硬件设备通过中断信号通知CPU,CPU进入中断处理程序。此过程中系统将关闭所有中断,防止其他中断打扰本次中断的执行,直到当前中断处理程序执行完成。如果某个中断处理程序执行时间过长,就可能会影响其他中断的处理。例如,如果网络收包中断处理时间非常长,就有可能影响键盘鼠标的输入,发生鼠标移动卡顿、键盘响应慢的情况。

所以为了提高系统响应速度,内核将中断分成了上半部和下半部两段。

上半部(Top-Half): 仅做最简单的工作,即将中断信息保存下来,尽快释放CPU。因为此时外部中断处于关闭状态,处理时间过长将会导致其他设备的中断得不到及时响应,影响系统整体性能。

下半部(Bottom-Half): 根据上半部保存的信息继续处理,下半部处理时外部中断已经打开,所以对实时性没有要求。对于网络应用来说,其下半部的处理工作位于ksoftirqd内核线程中。

通过上半部和下半部配合,就可以在不降低系统响应速度的前提下,提高系统吞吐能力。

回到网络收包流程中。网络设备收到报文后,通过硬件DMA(Direct Memory Access,直接内存访问)将数据报文从网卡设备内存(IO MEM)传送到系统内存(RAM)的报文缓冲区中,随后产生一个中断,通知CPU有数据报文到达。CPU收到中断请求后,调用网络设备驱动注册的中断向量处理报文。

图2-8展示了硬件设备收到报文后,以中断方式通知内核收包的整体流程。

图2-8 硬件接收报文后触发内核中断处理

该流程涉及以下步骤。

①网卡设备收到外部报文,硬件设备通过DMA将数据传输到系统内存的RX Ring Buffer(环形队列)中。

②硬件设备触发中断,通知CPU有数据报文到达。

③CPU收到中断请求后,调用对应的中断向量程序。Intel 10 Gigabit的网络设备(如Intel 82599)的中断服务处理程序为ixgbe_xxx()。

④内核进入中断处理的上半部,这里仅做简单检查,清除硬件标志,保存中断现场,触发软中断后退出。

⑤Linux内核软中断由内核线程ksoftirqd守护,驱动程序在上半部触发软中断后唤醒ksoftirqd线程,ksoftirqd检查是否有软中断请求达到。如果有则遍历待处理的软中断列表,找到该软中断编号对应的处理程序并调用,这时进入中断的下半部处理流程。

⑥ksoftirqd线程调用网络收包处理程序,该程序从环形队列中摘出报文,初步处理后上送内核协议栈继续处理。

下文将以Intel ixgbe网卡驱动为例说明处理流程。

1.启用网卡设备(用户空间)

网络设备的中断服务程序是在网卡启用时注册的,通过ifconfig/ip命令启用网卡触发此操作。例如,下面的命令用于启用eth0网络设备:

ifconfig命令位于net-tools工具包中,用户执行启用操作时,命令首先调用sockets_open()函数(位于net-tools/lib/sockets.c 文件中)创建socket对象,创建时指定socket类型为SOCK_DGRAM。创建完成后将此对象的文件描述符保存到skfd变量中,后续执行系统调用时使用。接着,调用set_f lag()函数给设备打上“(IFF_UP|IFF_RUNNING)”标志:

继续跟踪set_f lag()函数。set_f lag()首先通过ioctl系统调用(cmd=SIOCGIFFLAGS)获取当前网络设备参数(ioctl的第一个参数为前面创建的skfd),接着将“(IFF_UP|IFF_RUNNING)”标志追加到设备的“ifr.ifr_f lags”参数中,最后通过ioctl系统调用(cmd=SIOCSIFFLAGS)将标志写回设备。

2.启用网卡设备(内核空间)

ifconfig命令通过ioctl系统调用设置网卡状态为UP状态。ioctl系统调用的定义如下:

ioctl系统调用的cmd参数取值为SIOCSIFFLAGS(更改设备状态),系统调用的实际处理函数是vfs_ioctl()。跟踪vfs_ioctl()函数,该函数调用文件对应的“f_op->unlocked_ioctl”接口执行系统调用:

本次执行系统调用时传递的文件为socket文件,socket文件操作接口定义在socket_file_ops对象中,unlocked_ioctl成员指向的函数是sock_ioctl(),所以这里最终调用sock_ioctl()函数:

跟踪sock_ioctl()函数的调用栈,底层调用sock操作对象的ioctl接口:

在2.2.2节中,描述SOCK_DGRAM类型的sock操作对象是inet_dgram_ops,对象的ioctl接口设置为inet_ioctl()函数:

继续跟踪inet_ioctl()的调用栈:

__dev_change_f lags()函数根据用户请求的标志,决定打开还是关闭网卡:

本次执行打开操作,所以接下来调用__dev_open()函数打开设备。__dev_open()继续调用设备的ndo_open接口,实现设备的打开操作:

以Intel ixgbe网卡设备驱动程序为例,内核启动时调用设备发现接口,ixgbe驱动程序的probe接口向系统注册了ixgbe_netdev_ops对象,此对象描述了设备的打开接口:

本设备ndo_open接口为ixgbe_open()函数。每当网卡从DOWN状态变更为UP后,内核调用ixgbe_open()函数,该函数继续调用ixgbe_request_irq()实现中断向量的注册。相关调用栈如下:

ixgbe_request_irq()函数根据设备的配置情况,注册不同的中断处理函数:

ixgbe_request_irq()函数根据设备的支持能力注册中断处理函数。

➢设备支持MSIX模式时,中断处理函数为ixgbe_msix_clean_rings()。

➢设备支持MSI模式时,中断处理函数为ixgbe_intr()。

➢设备支持INTx模式的中断处理函数为ixgbe_intr()。

至此,设备已经就绪,内核开始处理从该设备接收的报文。

图2-9总结了硬件设备驱动注册中断处理函数的流程。

图2-9 设备注册中断处理函数的流程

3.中断处理流程

ixgbe驱动程序的ixgbe_request_irq()函数根据硬件设备能力注册中断处理函数,这些函数名称不同,但是最终的处理过程是极为相似的。

以INTx模式为例,ixgbe_intr()的调用栈如下:

当硬件设备收到报文后,首先通过硬件DMA将数据传输到系统内存中,然后触发中断。中断处理流程做一些初始化操作,执行结束后调用napi_schedule_irqoff()设置软中断标志NET_RX_SOFTIRQ,触发下半部执行。

上述调用过程中,____napi_schedule()函数比较关键,核心代码如下:

函数传递了如下两个参数。

struct softnet_data*sd: 该参数为当前CPU的softnet_data对象指针(每个CPU均对应一个softnet_data对象),此对象保存本CPU需要处理的收包任务列表。softnet_data结构中的poll_list成员保存了待执行收包操作的napi对象,此对象与设备绑定,凡是需要本CPU执行收包操作的napi对象均挂在此列表上。

struct napi_struct*napi: 一般情况下每个网络设备对应一个napi对象,此对象中保存了本设备用于处理收包的poll接口。如果当前设备的收包操作需要某个CPU去处理,那么就将本设备的napi对象挂载到该CPU softnet_data对象的poll_list列表中。

其数据结构参考图2-10。

___napi_schedule()首先调用list_add_tail()函数将当前设备的napi对象挂载到CPU softnet_data对象的poll_list列表中,然后调用__raise_softirq_irqoff()设置软中断待处理(pending)标志,本次调用传递的软中断类型为NET_RX_SOFTIRQ。接下来的流程就交给ksoftirqd线程了,网络接收报文的后半段处理流程均在ksoftirqd线程中执行。

跟踪整个napi_schedule_irqoff()函数会发现,此函数仅仅设置了CPU的pending状态,将需要处理的软中断编号打上标志,并没有触发软中断的调度。而软中断是通过内核线程ksoftirqd实现的,如果ksoftirqd没有得到调度,那软中断是不会被执行的。那么,内核是如何触发ksoftirqd线程调度的呢?

图2-10 CPU中softnet_data与napi_struct的关系

Intel 82599网卡驱动注册的中断回调函数为ixgbe_intr(),但是此函数并不是中断处理的入口函数,真正的中断入口函数位于内核中。内核的ASM_CALL_SYSVEC/ASM_CALL_IRQ宏定义了中断函数调用过程,以ASM_CALL_SYSVEC为例进行说明:

该宏首先调用irq_enter_rcu()进入中断处理,然后调用中断处理函数,待处理完成后调用irq_exit_rcu()退出中断处理。

irq_exit_rcu()继续调用__irq_exit_rcu()。跟踪__irq_exit_rcu()函数:

__irq_exit_rcu()在退出中断处理之前,函数会检查是否有挂起的软中断需要处理,如果有,则调用wakeup_softirqd()函数唤醒ksoftirqd线程。ksoftirqd得到调度后,就可以正常处理前面设置的软中断标志了。

2.3.2 中断处理下半部

中断处理下半部由内核线程ksoftirqd完成,该线程用于处理各种软中断请求。系统启动时为每个CPU创建了一个ksoftirqd内核线程,多核系统中多个CPU可以并发处理软中断。

要想弄清楚网络软中断下半部的处理流程,首先需要了解软中断的注册机制,通过此机制找到软中断编号NET_RX_SOFTIRQ对应的处理函数。然后深入中断处理函数,厘清整个处理流程。

1.创建ksoftirqd内核线程

内核线程ksoftirqd专门用于处理内核软中断请求。ksoftirqd定义如下:

内核在启动时调用spawn_ksoftirqd()函数,以为每个CPU绑定一个ksoftirqd线程:

注意

spawn_ksoftirqd()使用early_initcall修饰,此类函数会在main()函数之前执行,所以我们无法在内核中找到spawn_ksoftirqd()函数的调用点。

从softirq_threads结构定义可以看到,ksoftirqd的入口函数为run_ksoftirqd()。下面是run_ksoftirqd()函数的核心代码:

函数首先检查是否存在需要处理的软中断,如果有,则调用__do_softirq()函数处理。这里摘取__do_softirq()的核心代码进行参考:

函数首先得到软中断向量数组头softirq_vec,然后计算得到挂起的软中断编号,进而找到该编号对应的软中断处理对象,调用此对象的action接口。上面的ffs()函数用于获取指定数据的第一个有效比特位的位置,pending字段是一个u32类型的数据,所以软中断编号范围是0~31。目前内核支持的软中断列表如下:

网络接收报文对应的软中断编号为NET_RX_SOFTIRQ,接下来继续寻找NET_RX_SOFTIRQ对应的软中断处理函数(action函数)。

2.注册NET_RX_SOFTIRQ

内核初始化时调用net_dev_init()函数执行网络设备的初始化,该函数注册了两个softirq的处理函数,分别是NET_TX_SOFTIRQ和NET_RX_SOFTIRQ。

open_softirq函数将回调函数写入softirq_vec数组中:

所以,软中断编号NET_RX_SOFTIRQ对应的处理函数为net_rx_action(),软中断编号NET_RX_SOFTIRQ对应的处理函数为net_tx_action()。

3.软中断收包

继续讨论软中断处理函数,图2-11展示了软中断收包处理的总体流程。

NET_RX_SOFTIRQ软中断对应的处理函数为net_rx_action(),下面是该函数的部分关键代码:

图2-11 软中断收包处理流程

前面提到每个CPU对应了一个softnet_data对象,该对象中的poll_list成员中链接了所有需要执行接收报文操作的napi对象(每个napi对象对应了一个设备)。net_rx_action()函数首先获取本CPU的poll_list,遍历并逐个调用napi_poll()函数在当前设备上执行收包操作。

跟踪napi_poll()函数,调用栈如下:

底层调用napi对象的poll接口,该接口是在设备发现的时候注册的。

继续以Intel 82599设备为例,内核初始化时调用ixgbe_probe()发现设备,此函数根据当前系统配置情况来决定创建收发队列数量,在函数调用底层注册了poll函数,调用过程如下:

netif_napi_add()函数的第三个参数为poll回调,此函数将回调函数指针赋值给napi对象的poll成员:

所以,Intel 82599设备的函数为ixgbe_poll()。ixgbe_poll()函数的调用栈如下:

从napi_gro_receive()函数开始,内核进入通用处理流程,接下来的调用栈如下:

流程中需要注意以下几个关键技术点。

执行native XDP(eXpress Data Path)回调: XDP是Linux内核提供的高性能、可编程的网络转发路径,用户可以在此插入BPF程序实现自定义转发。

创建套接字缓冲区skb(struct sk_buff)资源: 可以说skb结构是Linux网络代码中最关键的数据结构,该结构中保存了IP报文所有分层(L2、L3、L4)的报文头信息,代码通过偏移量就可以快速找到报文头信息。该结构的具体内容请参考介绍内核原理的书籍,本节重点梳理报文处理流程,不再详细介绍此结构体信息。

执行generic XDP调用: 与native XDP类似,两者的不同在于调用的时机不同。部分设备驱动程序不支持native XDP调用,但是一定支持generic XDP调用。

执行调用栈底层的deliver_skb()函数: 将skb传递给上一层的协议层处理。

deliver_skb()函数的关键代码如下:

函数的第二个参数pt_prev的类型为struct packet_type,每种数据包类型均注册了自己的packet_type对象。IPv4类型的packet_type对象定义如下:

内核初始化时调用inet_init()将ip_packet_type对象注册到系统中,deliver_skb()分发IPv4报文时,最终调用ip_rcv()函数接收报文。

接下来进入IP协议层处理流程。

2.3.3 IP协议层处理

软中断最后调用的是网络协议类型关联的回调函数,IPv4协议的回调函数为ip_rcv(),此函数是所有IPv4报文的入口点。这里也是L2和L3的分界点,从现在开始进入L3协议层处理流程。

图2-12展示了IP协议层的处理流程。

图2-12 IP协议层处理流程

1.接收IP报文

L3协议层的入口函数为ip_rcv(),关键代码如下:

函数处理分为如下两部分。

ip_rcv_core: 报文完整性检查,仅做报文头解析、数据校验等工作。

NF_HOOK: Netfilter挂载点,此处将进行Netfilter回调,对应的挂载点为PREROUTING。如果Netfilter回调的执行结果为ACCEPT,则继续调用ip_rcv_finish()处理报文。

在整个网络协议栈处理流程中,Netfilter一共选定了5个挂载点,用户可以在这些挂载点注册钩子回调。图2-13展示了NF_INET_PRE_ROUTING在报文处理流程中的位置。

图2-13 Netfilter PREROUTING挂载点

NF_HOOK是一个inline函数,本例中该函数实现了Netfilter NF_INET_PRE_ROUTING挂载点的调用。

NF_HOOK函数定义:

该函数包含两部分内容。

调用nf_hook(): 执行iptables PREROUTING链上定义的规则。

调用传递进来的okfn函数: 外部调用NF_HOOK时传递进来的okfn参数值为ip_rcv_finish(),ip_rcv()函数继续调用此函数执行。

跟踪nf_hook(),该函数调用nf_hook_slow()实现完整的功能,关键代码如下:

nf_hook_slow()遍历PREROUTING链的所有回调函数并按序调用,执行完成后得到后续的处理策略,有如下三种可能的结果。

NF_ACCEPT: 接受,继续执行后续的报文处理。

NF_DROP: 丢弃报文。

NF_QUEUE: 将报文写入指定的队列中,交由用户空间程序决定该报文如何处理。例如,用户执行“iptables -A INPUT -j NFQUEUE --queue-num 0”命令向系统中添加规则,该规则是指将接收的报文全部送入0号队列,由用户空间的程序来决定报文的处理策略。用户空间程序使用libnetfilter_queue库连接0号队列,收到内核空间的请求后决策报文去向。

内核执行Netfilter规则时采用循环遍历机制,当Netfilter表项比较少时,执行规则的过程对整体转发性能影响有限,但是当表项数量过多时,此功能将严重影响性能。

如果nf_hook_entry_hookfn()的执行结果中返回的是NF_ACCEPT,则函数最后返回1,NF_HOOK继续调用ip_rcv_finish()接收报文。

2.路由查找

Netfilter调用NF_HOOK函数返回后,内核继续调用ip_rcv_finish()函数处理IP报文。ip_rcv_finish()的调用栈如下:

调用栈上第一个函数为ip_rcv_finish_core(),该函数的核心功能是进行路由查找。路由查找完成后得到dst_entry对象,该对象决定了当前报文的后续处理流程。

为了保证转发效率,路由查找过程分为两段。

skb_dst()(快路由查找): 直接查看当前报文的skb->_skb_refdst指针是否有效,如果有效则直接使用。此指针相当于cache(缓存)功能,只不过缓存里只有一条有效记录。

ip_route_input_slow()(慢路由流程): 如果skb->_skb_refdst无效则执行此流程,此函数在整个路由表范围内执行路由匹配。

慢路由查找的关键代码如下:

慢路由查找完成后得到该报文的后续处理策略。

本地接收: 当匹配的路由类型为LOCAL时(res->type==RTN_LOCAL),继续上送协议栈执行后续操作,调用ip_local_deliver()函数将报文传递给接收方。

转发: 当匹配的路由类型为UNICAST时(res->type==RTN_UNICAST),调用ip_forward()函数将报文转发出去。

3.本机接收报文

ip_rcv_finish()执行完路由查找后,若发现当前报文需要由本机接收,则继续调用dst_input()函数处理:

dst_input()设计为内联函数,其主体功能是调用INDIRECT_CALL_INET宏来判断后续调用哪个入口。

INDIRECT_CALL_INET宏的执行过程如下。

①如果参数满足f->input==f2,那么调用f2。

②如果参数满足f->input!=f2,那么继续判断是否满足f->input==f1,如果满足条件就调用f1。

③如果上述条件都不满足,就调用f->input()。

对于本地接收场景,已经设置skb_dst(skb)->input=ip_local_deliver(),所以后续的处理函数为ip_local_deliver()。继续跟踪ip_local_deliver()实现:

这里涉及Netfilter的第二个挂载点NF_INET_LOCAL_IN,此挂载点在流程中的位置参考图2-14。

图2-14 Netfilter INPUT挂载点

它与PREROUTING挂载点的执行过程相似,调用完成后继续调用ip_local_deliver_finish()。函数的调用栈如下:

ip_protocol_deliver_rcu()根据报文的协议类型决定后续如何处理:

如果当前报文的协议类型为UDP,那么就调用udp_rcv();如果是TCP,那么就调用tcp_v4_rcv()。本例实现的是UDP接收,所以最终调用的是udp_rcv()。

2.3.4 UDP协议层处理

IP协议层处理完成后进入UDP协议层,UDP协议层的处理函数为udp_rcv()。

图2-15展示了UDP协议层的处理流程。

图2-15 UDP协议层处理流程

UDP协议层的入口函数为udp_rcv(),调用栈如下:

__udp4_lib_rcv()根据四层协议信息找到报文关联的socket:

函数首先调用__udp4_lib_lookup_skb(),在udptable中查找此报文归属的socket。此函数将根据报文的源/目的地址、源/目的端口号结合,查找到对应的socket。如果函数找不到对应的socket,则内核会向发送方回复ICMP应答,错误码为“端口不可达”;如果找到了socket,则接着调用udp_unicast_rcv_skb()(不讨论组播报文的处理),将报文挂到用户socket队列上。udp_unicast_rcv_skb()的调用栈如下:

调用栈底层的__udp_enqueue_schedule_skb()函数会执行最终的挂队列操作。函数将报文挂到socket的sk_receive_queue队列上:

挂队列成功后调用sk->sk_data_ready(sk)来通知用户进程数据已就绪。若sk_data_ready是函数指针,那么这个指针指向哪个函数呢?

当用户创建socket时,内核调用sock_init_data()执行socket数据初始化:

这里设置sk_data_ready指针指向sock_def_readable()函数。

继续跟踪sock_def_readable()函数的实现:

sock_def_readable()检查是否有进程阻塞在此socket上,如果有就唤醒进程。

至此,内核态的报文接收流程就结束了,接下来进入用户进程收取报文。

2.3.5 用户进程接收报文

应用程序调用recvfrom()或者recv()接口接收报文,这两个接口最终都是执行系统调用的。内核代码中,两个系统调用的定义如下:

recv和recvfrom系统调用的参数略有差异:recv适用于有连接的套接字,即在使用该函数之前先调用connect()函数进行连接,通常用于TCP;recvfrom适用于无连接的套接字,即在使用该函数之前不需要执行连接操作,通常用于UDP。两个系统调用最终都会执行到内核的__sys_recvfrom()函数,调用栈如下:

__sys_recvfrom()首先调用sock_recvmsg()收取报文。底层调用__skb_recv_udp()尝试从socket的接收队列中获取数据,如果无法获取,则阻塞进程。下面是__skb_recv_udp()的核心代码:

__skb_recv_udp()优先从socket的reader_queue队列中获取数据,如果无法获取,则将socket的sk_receive_queue里面的报文转移到reader_queue队列中,然后继续在reader_queue队列上执行收取报文操作,直到无报文可接收,进程被阻塞。

__skb_recv_udp()函数返回有效数据,则证明从reader_queue队列中获取了有效报文。接下来,__sys_recvfrom()继续调用move_addr_to_user()将报文从内核空间复制到用户空间,系统调用完成并返回。

最后回到用户进程,代码如下:

当外部报文送达用户进程后,recvfrom()函数返回本次收到的报文大小以及发送端信息。

至此,UDP报文接收流程完成。

2.3.6 接收报文中断的亲和性设置

Linux系统提供中断亲和性(Affinity)配置功能,可以通过修改中断亲和性参数来将网卡中断绑定到特定的CPU核上。亲和性配置文件路径为“/proc/irq/<interrupt-number>/smp_affinity”。例如,本机环境的网卡设备中断编号为16,则该中断对应的绑定配置文件为“/proc/irq/16/smp_affinity”。亲和性配置文件内容为一个数值,每个CPU占用一个比特位,取值为0xFFFF表示该中断可以被CPU0~CPU15处理。假如将所有网卡的中断全部绑定到一个CPU上,如果网络流量过高,单个CPU处理不过来,就可能存在系统丢包的风险,所以对于流量要求比较高的场景,用户需合理设置中断亲和性。

测试环境exp主机的网卡设备占用16号中断,该中断的亲和性配置如下:

/proc/interrupts文件呈现了系统中断统计数据,结果如下:

从上面的输出可以看到,在exp主机上CPU0处理了绝大部分的网卡中断。接下来将中断绑到CPU1上:

通过enp0s8设备从外部网络中下载数据,再次查看中断统计数据:

CPU0上的统计数据变化不大,enp0s8网络设备的中断基本上都转移到CPU1上进行处理了。

2.3.7 报文接收流程总结

本节讨论了Linux操作系统中应用程序接收报文的完整流程,以网络设备接收报文为起点,设备以中断方式通知内核处理接收到的报文,然后经历L3、L4协议栈处理,最终将报文送到了用户应用程序上。

接收报文流程主要靠内核线程ksoftirqd,系统收取报文的能力与ksoftirqd线程的处理能力直接相关。在一些高网络带宽场景下,ksoftirqd将限制系统的最大处理能力。为了避免受内核限制,用户可以考虑使用用户态程序直接从网络设备收取报文,例如,基于DPDK开发的应用程序,系统的处理能力由用户态线程数/处理能力决定。

当然,很多事情没有万全之策,当缺少内核协议栈的支持时,用户态应用需要具备部分网络协议栈功能,代码复杂度也将直线上升。有得就有失,最终的选择还是取决于应用场景。

本节重点讨论的是系统接收报文的整体流程,对于细节部分并未进行详述,有兴趣的读者可以参考《深入理解Linux网络技术内幕》等专门讲述内核实现的书籍。 Nlex5uU441U8N6JpvPUzyLFZlPBHZLTvvFXAxyjlSNfj6eK8lsb663NCLE0saMgy

点击中间区域
呼出菜单
上一章
目录
下一章
×