2. 软中断机制:软中断的注册、触发、执行流程,软中断向量表

软中断(SoftIRQ)是 Linux 内核中断底半部机制中最核心、最底层的实现。tasklet 本质上是对软中断的封装与简化。理解软中断的注册、触发与执行流程,是掌握 tasklet 调试与竞态条件排查的前提。

2.1 软中断向量表与优先级

Linux 内核在 kernel/softirq.c 中定义了一个全局的软中断向量表 softirq_vec[],每个表项对应一个软中断处理函数。系统共支持 10 个软中断类型(NR_SOFTIRQS=10),其中高优先级软中断(HI_SOFTIRQ)优先于普通软中断(TASKLET_SOFTIRQ)执行。

索引 软中断名称 优先级 关联底半部
0 HI_SOFTIRQ 最高 高优先级 tasklet
1 TIMER_SOFTIRQ 高 定时器
2 NET_TX_SOFTIRQ 中 网络发送
3 NET_RX_SOFTIRQ 中 网络接收
4 BLOCK_SOFTIRQ 中 块设备
5 IRQ_POLL_SOFTIRQ 中 中断轮询
6 TASKLET_SOFTIRQ 低 普通 tasklet
7 SCHED_SOFTIRQ 低 调度
8 HRTIMER_SOFTIRQ 低 高精度定时器
9 RCU_SOFTIRQ 最低 RCU 回调
调试要点: 在 /proc/softirqs 中可以查看每个 CPU 上各软中断的累计触发次数。当 tasklet 出现异常时,首先检查 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 的计数是否异常增长。

2.2 软中断的注册流程

软中断的注册通过 open_softirq() 函数完成。该函数将处理函数指针写入 softirq_vec[] 的对应表项。注册时机通常在内核初始化阶段,驱动模块一般不需要直接注册软中断,而是通过 tasklet 接口间接使用。

// kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

注册流程的关键步骤:

  1. 分配软中断编号:使用预定义的枚举值(如 TASKLET_SOFTIRQ)。
  2. 编写处理函数:函数原型为 void action(struct softirq_action *)。
  3. 调用 open_softirq():在系统初始化或模块加载时注册。
  4. 激活软中断:通过 raise_softirq() 或 raise_softirq_irqoff() 触发。
竞态条件风险: 软中断处理函数在同一个 CPU 上不会嵌套执行,但不同 CPU 可以同时执行同一个软中断。这意味着软中断处理函数必须考虑 SMP 并发 问题,这是 tasklet 调试中常见的竞态根源。

2.3 软中断的触发机制

软中断的触发通过设置 __softirq_pending 位图实现。每个 CPU 维护一个本地软中断挂起位图,当某位被置 1 时,表示对应软中断需要被处理。

// 触发软中断的核心函数
void __raise_softirq_irqoff(unsigned int nr)
{
    trace_softirq_raise(nr);
    or_softirq_pending(1UL << nr);
}

void raise_softirq(unsigned int nr)
{
    unsigned long flags;
    local_irq_save(flags);
    __raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

触发时机通常发生在:

  • 硬件中断处理函数返回前:在 do_IRQ() 或 irq_exit() 中检查并执行软中断。
  • 显式调用:驱动代码调用 raise_softirq() 主动触发。
  • ksoftirqd 内核线程:当软中断执行时间过长或重入次数过多时,由内核线程接管。
调试陷阱: 在中断上下文调用 raise_softirq() 时,如果当前中断已经禁用了本地中断,必须使用 raise_softirq_irqoff() 以避免死锁。这是 tasklet 调度中常见的错误模式。

2.4 软中断的执行流程

软中断的执行入口是 __do_softirq(),该函数在 irq_exit() 或 ksoftirqd 中被调用。执行流程遵循严格的优先级顺序:从高优先级软中断开始,依次检查并执行。

// 简化后的执行流程
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    int max_restart = MAX_SOFTIRQ_RESTART;
    struct softirq_action *h;
    __u32 pending;
    int softirq_bit;

    pending = local_softirq_pending();
    // 重置挂起位图
    set_softirq_pending(0);

    // 按优先级遍历软中断向量表
    h = softirq_vec;
    while ((softirq_bit = ffs(pending))) {
        int vec_nr = --softirq_bit;
        h += vec_nr;
        // 执行软中断处理函数
        h->action(h);
        h++;
        pending >>= softirq_bit + 1;
    }

    // 如果时间或重入次数超限,唤醒 ksoftirqd
    if (__this_cpu_read(ksoftirqd) && ksoftirqd_running(ksoftirqd))
        wakeup_softirqd();
}
性能指标: MAX_SOFTIRQ_TIME 默认为 2 毫秒,MAX_SOFTIRQ_RESTART 默认为 10 次。如果软中断执行超过这些限制,剩余工作会交给 ksoftirqd 内核线程处理,这会导致调度延迟增加。

2.5 软中断上下文与进程上下文的区别

特性 软中断上下文 进程上下文
可睡眠 否(禁止调度) 是
可抢占 否(除非开启 PREEMPT_RT) 是
持有锁 只能使用 spinlock 可使用 mutex
访问用户空间 不允许 允许
当前进程指针 指向被中断的进程 指向当前进程
调试关键: 在软中断上下文中调用 schedule() 或 msleep() 会导致内核崩溃。使用 in_softirq() 宏可以在运行时检测当前是否处于软中断上下文,这是编写安全 tasklet 回调的基本防御手段。

2.6 软中断执行流程 SVG 架构图

软中断执行流程架构图 硬件中断触发 (do_IRQ) irq_exit() 检查软中断挂起 有挂起软中断? 是 否 __do_softirq() 主循环 遍历 softirq_vec[] 表 执行 h->action(h) ksoftirqd 接管条件 执行时间 > 2ms 重入次数 > 10 唤醒 ksoftirqd 线程 返回中断现场

2.7 软中断与 tasklet 的关系

tasklet 是软中断的轻量级封装。每个 tasklet 最终通过 TASKLET_SOFTIRQ 或 HI_SOFTIRQ 软中断执行。理解这种关系对于调试至关重要:

  • 调度机制:tasklet_schedule() 将 tasklet 加入 per-CPU 链表,然后触发 TASKLET_SOFTIRQ。
  • 执行顺序:同一 CPU 上的 tasklet 按 FIFO 顺序执行,但不同 CPU 上的 tasklet 可以并行。
  • 重入保护:tasklet 本身保证同一 tasklet 不会在多个 CPU 上同时执行,但不同 tasklet 之间没有保护。
竞态条件根源: 虽然 tasklet 自身有重入保护,但多个 tasklet 共享全局数据时,由于它们运行在软中断上下文中且可能被不同 CPU 并行执行,必须使用 spinlock 保护共享资源。这是 tasklet 调试中最常见的竞态条件类型。

2.8 调试软中断的常用方法

调试方法 工具/接口 适用场景
统计计数 /proc/softirqs 查看各 CPU 软中断触发频率
跟踪点 trace_softirq_raise/entry/exit ftrace 跟踪软中断执行时序
上下文检测 in_softirq() / in_interrupt() 运行时判断当前上下文
锁检测 CONFIG_DEBUG_SPINLOCK 检测软中断中的锁使用错误
延迟测量 softirq_timing 内核模块 测量软中断执行时间
实战建议: 在开发阶段开启 CONFIG_DEBUG_ATOMIC_SLEEP 和 CONFIG_PROVE_LOCKING,可以在软中断上下文中误用睡眠锁时立即触发内核警告,这是捕获 tasklet 竞态条件的第一道防线。