软中断
在研究网络栈之前,我们需要稍微了解一下 Linux 内核名为软中断的东西。
什么是软中断?
Linux 内核中的软中断系统是一种在驱动程序中实现的中断处理程序上下文之外执行代码的机制。这个系统很重要,因为在中断处理程序的全部或部分执行期间,硬件中断可能被禁用。中断被禁用的时间越长,错过事件的机会就越大。因此,推迟任何长时间运行的操作到中断处理程序之外是很重要的,以便它能尽快完成并重新启用来自设备的中断。
内核中还有其他机制推迟工作,但对于网络栈,我们将探讨 softirq。
可以将 softirq 系统想象为一系列内核线程(每个 CPU 一个),它们运行已为不同 softirq 事件注册的处理程序函数。如果您曾经查看过 top 并在内核线程列表中看到 ksoftirqd/0
,那么您正在查看在 CPU 0 上运行的 softirq 内核线程。
内核子系统(如网络)可以执行 open_softirq
函数来注册软中断处理程序。我们稍后将看到网络系统如何注册其软中断处理程序。现在,让我们了解更多关于软中断如何工作的信息。
ksoftirqd
既然软中断对于推迟设备驱动程序的工作非常重要,您可能会想象内核生命周期早期就会产生 ksoftirqd
进程,这是正确的。
查看 kernel/softirq.c 中的代码,可以发现 ksoftirqd
系统是如何初始化的。
static struct smp_hotplug_thread softirq_threads = { .store = &ksoftirqd, .thread_should_run = ksoftirqd_should_run, .thread_fn = run_ksoftirqd, .thread_comm = "ksoftirqd/%u", }; static __init int spawn_ksoftirqd(void) { register_cpu_notifier(&cpu_nfb); BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); return 0; } early_initcall(spawn_ksoftirqd);
从上面的 struct smp_hotplug_thread
定义中可以看出,注册了两个函数指针:ksoftirqd_should_run
和 run_ksoftirqd
。
作为类似于事件循环的一部分,这两个函数都是从 kernel/smpboot.c 中调用的。
kernel/smpboot.c
中的代码首先调用 ksoftirqd_should_run
,确定是否有未决的软中断,如果有未决的软中断,则执行 run_ksoftirqd
。run_ksoftirqd
在调用 __do_softirq
之前进行了一些小的簿记工作。
__do_softirq
__do_softirq
函数做了一些有趣的事情:
- 确定哪个软中断处于未决状态
- 出于统计目的,记录软中断时间
- 增加软中断执行统计
- 执行未决软中断的软中断处理程序(已调用
open_softirq
注册)。
因此,当您查看 CPU 使用率图表并看到 softirq
或 si
时,您现在知道这是在测量延迟工作上下文中的 CPU 使用量。
监控
/proc/softirqs
softirq
系统增加统计计数器,可以从 /proc/softirqs
读取。监控这些统计数据可以让您了解各种事件的软中断产生的速率。
读取 /proc/softirqs
检查软中断统计信息。
$ cat /proc/softirqs CPU0 CPU1 CPU2 CPU3 HI: 0 0 0 0 TIMER: 2831512516 1337085411 1103326083 1423923272 NET_TX: 15774435 779806 733217 749512 NET_RX: 1671622615 1257853535 2088429526 2674732223 BLOCK: 1800253852 1466177 1791366 634534 BLOCK_IOPOLL: 0 0 0 0 TASKLET: 25 0 0 0 SCHED: 2642378225 1711756029 629040543 682215771 HRTIMER: 2547911 2046898 1558136 1521176 RCU: 2056528783 4231862865 3545088730 844379888
这个文件可以让您了解您的网络接收(NET_RX
)处理当前如何分布在您的 CPU 上。如果分布不均匀,您会看到某些 CPU 的计数值比其他 CPU 大。这是一个指示器,表明您可能会从下面描述的 Receive Packet Steering / Receive Flow Steering 中受益。在监控性能时要小心使用这个文件:在网络活动高峰期,您可能会期望看到 NET_RX
增量率增加,但这并不一定是这样。事实证明,这有点微妙,因为网络栈中还有其他调节旋钮会影响 NET_RX
软中断触发的速率,我们很快就会看到。
但是,您应该注意到这一点,以便在调整其他调节旋钮时,您将知道检查 /proc/softirqs
并期望看到变化。
现在,让我们继续探讨网络栈,并从上到下追踪网络数据的接收方式。
Linux 网络设备子系统
现在我们已经了解了网络驱动程序和软中断是如何工作的,让我们看看 Linux 网络设备子系统是如何初始化的。 然后,我们可以从数据包的到达开始跟踪数据包的路径。
网络设备子系统初始化
网络设备(netdev)子系统在函数 net_dev_init
中初始化。 这个初始化函数中发生了很多有趣的事情。
struct softnet_data
结构初始化
net_dev_init
为系统的每个 CPU 创建一组 struct softnet_data
结构。这些结构将保存指向处理网络数据的几个重要内容的指针:
- 注册到此 CPU 的 NAPI 结构列表。
- 数据处理的积压。
- 处理
weight
。 - 接收卸载 结构列表。
- Receive packet steering 设置。
- 更多。
随着我们在网络栈中向上移动,将更详细地探讨这些点。
软中断处理程序的初始化
net_dev_init
注册一个发送和接收软中断处理程序,它将处理传入或传出的网络数据。 这段代码非常简单:
static int __init net_dev_init(void) { /* ... */ open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); /* ... */ }
我们很快就会看到驱动程序的中断处理程序如何“引发”(或触发)注册到 NET_RX_SOFTIRQ
软中断的 net_rx_action
函数。
数据到达
终于,网络数据到达了!
假设接收队列有足够的可用描述符,数据包将通过 DMA 写入 RAM。然后设备引发分配给它的中断(或者在 MSI-X 的情况下,与数据包到达的接收队列绑定的中断)。
中断处理程序
通常,当中断被引发时,运行的中断处理程序应该尽量推迟尽可能多的处理到中断上下文之外发生。这至关重要,因为在处理中断时,其他中断可能会被阻塞。
让我们看一下 MSI-X 中断处理程序的源代码;它将真正有助于说明中断处理程序尽可能少地工作的理念。
来自 drivers/net/ethernet/intel/igb/igb_main.c:
static irqreturn_t igb_msix_ring(int irq, void *data) { struct igb_q_vector *q_vector = data; /* Write the ITR value calculated from the previous interrupt. */ igb_write_itr(q_vector); napi_schedule(&q_vector->napi); return IRQ_HANDLED; }
这个中断处理程序非常短,执行 2 个非常快速的操作后返回。
首先,此函数调用 igb_write_itr
,它只更新一个硬件特定的寄存器。在这种情况下,更新的寄存器是跟踪硬件中断到达速率的寄存器。
此寄存器与称为“中断节流”(也称为“中断合并”)的硬件功能结合使用,可控制中断传递到 CPU 的速度。我们很快就会看到 ethtool
如何提供一种调整 IRQ 触发速率的机制。
其次,调用 napi_schedule
,如果 NAPI 处理循环尚未激活,则唤醒它。请注意,NAPI 处理循环在软中断中执行;NAPI 处理循环不从中断处理程序执行。中断处理程序只是使其开始执行(如果尚未执行)。
显示如何工作的实际代码非常重要;它将指导我们了解如何在多 CPU 系统上处理网络数据。
NAPI 和 napi_schedule
让我们弄清楚硬件中断处理程序中的 napi_schedule
调用是如何工作的。
请记住,NAPI 的存在是为了在不需要来自 NIC 的中断来信号数据准备好处理的情况下收集网络数据。如前所述,NAPI poll
循环是接收硬件中断引导的。换句话说:NAPI 已启用,但关闭,直到第一个数据包到达时,NIC 引发硬件中断并启动 NAPI。正如我们很快就会看到的那样,还有一些其他情况,其中 NAPI 可能被禁用,并且需要引发硬件中断才能再次启动。
当驱动程序中的中断处理程序调用 napi_schedule
时,将启动 NAPI 轮询循环。napi_schedule
实际上只是一个在头文件中定义的包装函数,它调用 __napi_schedule
。
来自 net/core/dev.c:
/** * __napi_schedule - schedule for receive * @n: entry to schedule * * The entry's receive function will be scheduled to run */ void __napi_schedule(struct napi_struct *n) { unsigned long flags; local_irq_save(flags); ____napi_schedule(&__get_cpu_var(softnet_data), n); local_irq_restore(flags); } EXPORT_SYMBOL(__napi_schedule);
这段代码使用 __get_cpu_var
获取注册到当前 CPU 的 softnet_data
结构。这个 softnet_data
结构和从驱动程序传递上来的 struct napi_struct
结构被传递到 ____napi_schedule
。哇,这是很多下划线 ;)
让我们看一下 ____napi_schedule
,来自 net/core/dev.c:
/* Called with irq disabled */ static inline void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi) { list_add_tail(&napi->poll_list, &sd->poll_list); __raise_softirq_irqoff(NET_RX_SOFTIRQ); }
这段代码做了两件重要的事情:
- 从设备驱动程序的中断处理程序代码传递上来的
struct napi_struct
,被添加到与当前 CPU 关联的softnet_data
结构的poll_list
中。 - 使用
__raise_softirq_irqoff
来“引发”(或触发)NET_RX_SOFTIRQ 软中断。将执行(如果当前未执行)在网络设备子系统初始化期间注册的net_rx_action
。
正如我们很快就会看到的那样,软中断处理函数 net_rx_action
将调用 NAPI poll
函数来收集数据包。
关于 CPU 和网络数据处理的说明
请注意,我们迄今为止看到的所有推迟硬件中断处理程序中的工作到 softirq 的代码,都使用了与当前 CPU 相关联的结构。
虽然驱动程序的硬中断处理程序本身所做的工作非常少,但软中断处理程序在与驱动程序的硬中断处理程序相同的 CPU 上执行。
这就是为什么设置硬中断处理的 CPU 处理很重要:该 CPU 不仅执行驱动程序中的中断处理程序,而且在 NAPI 以软中断方式收集数据包时也将使用相同的 CPU。
正如我们稍后将看到的,像 Receive Packet Steering 的功能可以将一些工作分配给网络栈更高层级的其他 CPU。