3. Linux 软中断机制:ksoftirqd 内核线程、软中断向量表、触发与执行流程
软中断(SoftIRQ)是 Linux 内核中实现下半部(Bottom Half)处理的核心机制,专门用于处理对时间要求严格但允许被硬中断打断的任务。在收包路径中,网卡驱动的硬中断处理程序仅完成最必要的操作(如拷贝寄存器、更新描述符),而将耗时的协议栈处理委托给软中断。本章节深入剖析软中断的三大支柱:ksoftirqd 内核线程、软中断向量表以及触发与执行流程。
3.1 软中断向量表:静态注册与动态索引
Linux 内核使用一个全局的静态数组 softirq_vec[] 来管理所有软中断类型,每个数组元素对应一个软中断处理函数。该表在系统初始化时由 open_softirq() 函数注册,运行时通过索引快速定位。
| 索引 | 软中断名称 | 关联宏 | 典型用途 |
|---|---|---|---|
| 0 | HI_SOFTIRQ | raise_softirq_irqoff | 高优先级 tasklet |
| 1 | TIMER_SOFTIRQ | raise_softirq_irqoff | 定时器回调 |
| 2 | NET_TX_SOFTIRQ | raise_softirq_irqoff | 网络数据包发送 |
| 3 | NET_RX_SOFTIRQ | __raise_softirq_irqoff | 网络数据包接收(NAPI 轮询入口) |
| 4 | BLOCK_SOFTIRQ | raise_softirq_irqoff | 块设备 I/O 完成 |
| 5 | IRQ_POLL_SOFTIRQ | raise_softirq_irqoff | 中断轮询 |
| 6 | TASKLET_SOFTIRQ | raise_softirq_irqoff | 常规 tasklet |
| 7 | SCHED_SOFTIRQ | raise_softirq_irqoff | 调度器负载均衡 |
| 8 | HRTIMER_SOFTIRQ | raise_softirq_irqoff | 高精度定时器 |
| 9 | RCU_SOFTIRQ | raise_softirq_irqoff | RCU 回调处理 |
关键点:NET_RX_SOFTIRQ(索引 3)是收包路径的核心。当网卡驱动在硬中断中调用
napi_schedule() 时,最终会通过 __raise_softirq_irqoff(NET_RX_SOFTIRQ) 将该软中断标记为待处理状态。
// 注册软中断处理函数示例(内核源码 kernel/softirq.c 简化)
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
// 网络子系统初始化时注册 NET_RX_SOFTIRQ
static int __init net_dev_init(void)
{
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action); // NAPI 轮询入口
return 0;
}
3.2 触发与执行流程:从硬中断到软中断的接力
软中断的执行并非由硬件直接触发,而是由硬中断处理程序在退出前“标记”待处理,然后在特定的安全上下文中被调度执行。完整的触发与执行流程分为三个阶段:
- 硬中断标记阶段:网卡产生硬件中断 → CPU 执行驱动注册的硬中断处理程序(ISR) → ISR 调用
napi_schedule()→ 最终调用__raise_softirq_irqoff(NET_RX_SOFTIRQ)在本地 CPU 的irq_cpustat_t中置位软中断标志位。 - 软中断检查阶段:硬中断处理程序返回后,CPU 在
irq_exit()函数中检查是否有待处理的软中断。如果存在,则调用invoke_softirq()尝试直接执行。 - 执行与唤醒阶段:
- 直接执行:如果当前上下文允许(不在硬中断中、不处于中断嵌套),CPU 直接在
irq_exit()中调用do_softirq()执行软中断处理函数。 - 唤醒 ksoftirqd:如果软中断执行时间过长(超过
MAX_SOFTIRQ_TIME或重试次数超过MAX_SOFTIRQ_RESTART),或者当前上下文不允许直接执行(如在硬中断中),则唤醒每个 CPU 上专用的ksoftirqd内核线程来接管处理。
- 直接执行:如果当前上下文允许(不在硬中断中、不处于中断嵌套),CPU 直接在
性能提示:直接执行路径延迟最低,适合低负载场景。高负载下,频繁的软中断会耗尽 CPU 时间,此时 ksoftirqd 的调度机制可以防止用户态进程被饿死,但会引入上下文切换开销。
3.3 ksoftirqd 内核线程:软中断的“守护进程”
每个 CPU 核心都拥有一个名为 ksoftirqd/N(N 为 CPU 编号)的内核线程。该线程的优先级设置为 MAX_RT_PRIO - 1(即 99,属于实时优先级范围),确保其能够及时处理软中断。
3.3.1 线程生命周期与调度
- 创建:系统启动时,
spawn_ksoftirqd()函数为每个在线 CPU 创建 ksoftirqd 线程。 - 休眠:当没有待处理的软中断时,线程调用
schedule()进入休眠状态,不消耗 CPU 资源。 - 唤醒:当
irq_exit()判断需要延迟处理时,通过wake_up_process()唤醒对应 CPU 的 ksoftirqd 线程。 - 处理循环:线程被唤醒后,执行
run_ksoftirqd()函数,该函数循环调用__do_softirq()处理所有待处理的软中断,直到满足退出条件(如无待处理软中断或时间片耗尽)。
// ksoftirqd 线程核心处理逻辑(简化自 kernel/softirq.c)
static int run_ksoftirqd(void *__bind_cpu)
{
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
if (local_softirq_pending()) {
__do_softirq(); // 执行软中断
cond_resched(); // 主动让出 CPU,防止饿死用户态进程
}
schedule(); // 无任务时休眠
}
return 0;
}
3.3.2 ksoftirqd 与直接执行的权衡
| 特性 | 直接执行(irq_exit 中) | ksoftirqd 执行 |
|---|---|---|
| 执行上下文 | 中断上下文(硬中断退出前) | 进程上下文(内核线程) |
| 延迟 | 极低(微秒级) | 较低(可能因调度引入毫秒级延迟) |
| 对用户态影响 | 抢占用户态进程,可能导致高延迟 | 通过调度器公平共享 CPU,保护用户态进程 |
| 适用场景 | 低负载、低延迟敏感 | 高负载、需要保证系统整体公平性 |
| 可配置性 | 由内核自动决定 | 可通过 /proc/irq/ 调整亲和性 |
性能陷阱:在极端高负载下,如果 ksoftirqd 线程被频繁唤醒且处理时间过长,会导致
ksoftirqd 的 CPU 使用率接近 100%。此时应检查 NAPI 轮询权重(net.core.netdev_budget)是否过大,或考虑使用 RPS(Receive Packet Steering)将负载分散到多个 CPU。
3.4 软中断执行的核心函数:__do_softirq()
无论是直接执行还是 ksoftirqd 执行,最终都会进入 __do_softirq() 函数。该函数按照软中断优先级(索引越小优先级越高)遍历 softirq_vec[] 表,执行被标记的软中断处理函数。
// __do_softirq 核心逻辑(简化自 kernel/softirq.c)
asmlinkage __visible void __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
int pending;
pending = local_softirq_pending(); // 获取待处理软中断位图
set_softirq_pending(0); // 清除位图
while (pending) {
h = softirq_vec;
do {
if (pending & 1) {
h->action(h); // 执行注册的处理函数
}
h++;
pending >>= 1;
} while (pending);
pending = local_softirq_pending();
if (pending) {
if (time_after(jiffies, end) || --max_restart < 0)
break; // 超时或重试次数耗尽,唤醒 ksoftirqd
set_softirq_pending(0);
}
}
}
关键机制:
MAX_SOFTIRQ_TIME(默认 2 毫秒)和 MAX_SOFTIRQ_RESTART(默认 10 次)是防止软中断饿死 CPU 的“安全阀”。当软中断执行超过这两个阈值时,内核会强制唤醒 ksoftirqd 线程,将后续处理转移到进程上下文,从而保证硬中断和用户态进程的响应性。
3.5 本章小结
- 软中断向量表是静态定义的索引数组,NET_RX_SOFTIRQ 是网络收包的核心入口。
- 触发流程由硬中断标记开始,经
irq_exit()检查,最终选择直接执行或唤醒ksoftirqd。 - ksoftirqd 内核线程是软中断的“安全网”,在高负载下防止 CPU 被软中断完全占据,但会引入调度开销。
- __do_softirq() 通过时间与重试次数双重限制,确保软中断处理的公平性与实时性。