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 触发与执行流程:从硬中断到软中断的接力

软中断的执行并非由硬件直接触发,而是由硬中断处理程序在退出前“标记”待处理,然后在特定的安全上下文中被调度执行。完整的触发与执行流程分为三个阶段:

  1. 硬中断标记阶段:网卡产生硬件中断 → CPU 执行驱动注册的硬中断处理程序(ISR) → ISR 调用 napi_schedule() → 最终调用 __raise_softirq_irqoff(NET_RX_SOFTIRQ) 在本地 CPU 的 irq_cpustat_t 中置位软中断标志位。
  2. 软中断检查阶段:硬中断处理程序返回后,CPU 在 irq_exit() 函数中检查是否有待处理的软中断。如果存在,则调用 invoke_softirq() 尝试直接执行。
  3. 执行与唤醒阶段:
    • 直接执行:如果当前上下文允许(不在硬中断中、不处于中断嵌套),CPU 直接在 irq_exit() 中调用 do_softirq() 执行软中断处理函数。
    • 唤醒 ksoftirqd:如果软中断执行时间过长(超过 MAX_SOFTIRQ_TIME 或重试次数超过 MAX_SOFTIRQ_RESTART),或者当前上下文不允许直接执行(如在硬中断中),则唤醒每个 CPU 上专用的 ksoftirqd 内核线程来接管处理。
性能提示:直接执行路径延迟最低,适合低负载场景。高负载下,频繁的软中断会耗尽 CPU 时间,此时 ksoftirqd 的调度机制可以防止用户态进程被饿死,但会引入上下文切换开销。
软中断触发与执行流程(从硬中断到 ksoftirqd) ① 硬中断到达 网卡触发 IRQ ② ISR 执行 napi_schedule() ③ 标记 NET_RX_SOFTIRQ __raise_softirq_irqoff() ④ irq_exit() 检查软中断 invoke_softirq() 允许直接执行? ⑤ 直接执行 do_softirq() 低延迟路径 ⑥ 唤醒 ksoftirqd/N 内核线程上下文处理

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() 通过时间与重试次数双重限制,确保软中断处理的公平性与实时性。