从初学者角度,介绍 Linux 内核如何接收网络帧:从网卡设备完成数据帧的接收开始,到数据帧被传递到网络栈中的三层结束。重点介绍内核的工作机制,不会深入过多代码层面的细节,示例代码来自 Linux 2.6。
设备的通知手段
从计算机硬件的角度,一个数据帧从进入网卡到最后被内核处理的整体示意图如下:
当网卡设备完成一个数据帧的接收后,可能将数据帧暂存于设备内存,也可能通过 DMA(Direct memory access) 直接写入到主内存的接收环(rx ring),接着必须通知操作系统内核对已接收的数据进行处理。下面将讨论几种可能的通知手段。
轮询
轮询(Polling)指的是由内核主动地去检查设备,比如定期读取设备的内存寄存器,判断是否有新的接收帧需要处理。这种方式在设备负载较高时响应效率低,在设备负载低时又占用系统资源,操作系统很少单独采用,结合其他机制后才能实现较理想的效果。
硬件中断
当接收到新的数据帧等事件发生时,设备将生成一个硬件中断信号。该信号通常由设备发送给中断控制器,由中断控制器分发给 CPU。CPU 接受信号后将从当前执行的任务中被打断,转而执行由设备驱动注册的中断处理程序来处理设备事件。中断处理程序会将数据帧拷贝到内核的输入队列中,并通知内核做进一步处理。这种技术在低负载时表现良好,因为每一个数据帧都会得到及时响应,但在负载较高时,CPU 会被频繁的中断从而影响到其他任务的执行。
对接收帧的处理通常分为两个部分:首先驱动注册的中断处理程序将帧复制到内核可访问的输入队列中,然后内核将其传递给相关协议的处理程序直到最后被应用程序消费。第一部分的中断处理程序是在中断上下文中执行的,可以抢占第二部分的执行,这意味着复制接收帧到输入队列的程序比消费数据帧的协议栈程序有更高的优先级。
在高流量负载下,中断处理程序会不断抢占 CPU。后果显而易见:输入队列最终将被填满,但应该去出队并处理这些帧的程序处于较低优先级没有机会执行。结果新的接收帧因为输入队列已满无法加入队列,而旧的帧因为没有可用的 CPU 资源不会被处理。这种情况被称为接收活锁(receive-livelock)。
硬件中断的优点是帧的接收和处理之间的延迟非常低,但在高负载下会严重影响其他内核或用户程序的执行。大多数网络驱动会使用硬件中断的某种优化版本。
一次处理多个帧
一些设备驱动会采用一种改良方式,当中断处理程序被执行时,会在指定的窗口时间或帧数量上限内持续地入队数据帧。由于中断处理程序执行时其他中断将被禁用,因此必须设置合理的执行策略来和其他任务共享 CPU 资源。
该方式还可进一步优化,设备仅通过硬件中断来通知内核有待处理的接收帧,将入队并处理接收帧的工作交给内核的其他处理程序来执行。这也是 Linux 的新接口 NAPI 的工作方式。
在实践中的组合
不同的通知机制有其适合的工作场景:低负载下纯中断模型保证了极低延迟,但在高负载下表现糟糕;计时中断在低负载下可能会引入过高延迟并浪费 CPU 时间,但在高负载下对减少 CPU 占用和解决接收活锁有很大帮助。在实践中,网络设备往往不依赖某种单一模型,而是采取组合方案。
以 Linux 2.6 Vortex 设备所注册的中断处理函数 vortex_interrupt(位于 /drivers/net/3c59x.c)为例:
设备会将多个事件归类为一种中断类型(甚至还可以在发送中断信号前等待一段时间,将多个中断聚合成一个信号发送)。中断触发 vortex_interrupt 的执行并禁用该 CPU 上的中断。
如果中断是由接收帧事件 RxComplete 引发,处理程序调用其他代码处理设备接收的帧。
vortex_interrupt 在执行期间持续读取设备寄存器,检查设备是否有新的中断信号发出。如果有且中断事件为 RxComplete,处理程序将继续处理接收帧,直到已处理帧的数量达到预设的 work_done值才结束。而其他类型的中断将被处理程序忽略。
软中断处理机制
当硬件中断信号到达 CPU 后,需要通过合理的任务调度机制,才能以较低延迟处理接收帧,又避免接收活锁和饥饿等资源抢占问题。
一个中断通常会触发以下事件:
设备产生一个中断并通过硬件通知内核。
如果内核没有正在处理另一个中断(即中断没有被禁用),它将收到这个通知。
内核禁用本地 CPU 的中断,并执行与收到的中断类型相关联的处理程序。
内核退出中断处理程序,重新启用本地 CPU 的中断。
CPU 在执行中断号对应处理程序的期间处于中断上下文,中断会被禁用。这意味着 CPU 在处理某个中断期间,它既不会处理其他中断,也不能被其他进程抢占,CPU 资源由该中断处理程序独占。这种设计决定减少了竞争条件的可能性,但也带来了潜在的性能影响。
显然,中断处理程序应当尽可能快地完成工作。不同的中断事件所需要的处理工作量并不相同,比如当键盘的按键被按下时,触发的中断处理函数只需要将该按键的编码记录下来,而且这种事件的发生频率不会很高;而处理网络设备收到的新数据帧时,需要为 skb 分配内存空间,拷贝接收到的数据,同时完成一些初始化工作比如判断数据所属的网络协议等。
为了尽量减少 CPU 处于中断上下文的时间,操作系统为中断处理程序引入了上、下半部的概念。
下半部处理程序
即使由中断触发的处理动作需要大量的 CPU 时间,大部分动作通常是可以等待的。中断可以第一时间抢占 CPU 执行,因为如果操作系统让硬件等待太长时间,硬件可能会丢失数据。这既适用于实时的数据,也适用于在固定大小缓冲区中存储的数据。如果硬件丢失了数据,一般没有办法再恢复(不考虑发送方重传的情况)。 另一方面,内核或用户空间的进程被推迟执行或抢占时,一般不会有什么损失(对实时性有极高要求的系统除外,它需要用完全不同的方式来处理进程和中断)。
服务电话: 400-678-1800 (周⼀⾄周五 09:00-18:00)
商务合作: 0571-87770835
市场反馈: marketing@woqutech.com
地址: 杭州市滨江区滨安路1190号智汇中⼼A座1101室