2. 软中断机制

软中断(SoftIRQ)是 Linux 内核中断下半部最底层的机制,也是 tasklet 和高精度定时器(hrtimer)等更高层机制的实现基石。理解软中断的注册、触发与执行流程,是掌握 tasklet 行为特征的前提。

2.1 软中断的核心数据结构

内核使用一个静态数组管理所有软中断,每个软中断对应一个描述符。软中断数量在编译时固定,最多 32 个,目前内核实际使用了约 10 个。

数据结构 作用 关键字段
softirq_vec[NR_SOFTIRQS] 全局软中断描述符数组 action 函数指针、data 参数
irq_cpustat_t __softirq_pending 每个 CPU 的挂起软中断位图 32 位掩码,每位对应一个软中断号
struct task_struct 中的 thread_info 标记当前进程是否需要处理软中断 TIF_NEED_RESCHED 等标志位
关键点:软中断是 全局静态注册 的,不能动态创建或销毁。每个软中断在系统初始化时通过 open_softirq() 绑定处理函数,之后在整个系统生命周期内保持不变。

2.2 软中断的注册流程

软中断的注册在系统启动早期完成,典型调用链如下:

  1. 定义软中断号:在 <linux/interrupt.h> 中枚举,如 HI_SOFTIRQ、TASKLET_SOFTIRQ、NET_TX_SOFTIRQ 等。
  2. 编写处理函数:函数原型为 void (*action)(struct softirq_action *)。
  3. 调用 open_softirq():将处理函数注册到 softirq_vec[nr]。
/* 注册示例:网络 TX 软中断 */
open_softirq(NET_TX_SOFTIRQ, net_tx_action);

/* 注册示例:tasklet 软中断 */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
注意:软中断处理函数运行在 硬中断上下文(中断关闭或开启取决于具体架构),不能睡眠,不能访问用户空间,不能持有信号量。

2.3 软中断的触发机制

触发软中断的核心函数是 raise_softirq() 和 raise_softirq_irqoff()。触发过程本质上是设置当前 CPU 的 __softirq_pending 位图中的对应位。

/* 触发软中断(带 irq 保存) */
void raise_softirq(unsigned int nr)
{
    unsigned long flags;
    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

/* 触发软中断(已关中断时使用) */
inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);
    /* 如果不在硬中断上下文,唤醒 ksoftirqd */
    if (!in_interrupt())
        wakeup_softirqd();
}
触发时机:软中断不会立即执行,而是标记为“待处理”。实际执行发生在以下三个入口之一:
① 硬中断处理程序退出时(irq_exit())
② 显式调用 local_bh_enable() 时
③ ksoftirqd 内核线程被唤醒时

2.4 软中断的执行流程

软中断的执行入口是 __do_softirq(),它遍历当前 CPU 的挂起位图,依次执行已触发的软中断处理函数。执行流程如下图所示:

软中断执行流程 硬中断退出 / 显式调用 读取 __softirq_pending while (pending != 0) 取 pending 最低置位位 执行 softirq_vec[nr]->action 清除已处理位,继续循环 执行限制 • 最多执行 10 次循环 • 超时唤醒 ksoftirqd • 允许抢占点检查
重要限制:为防止软中断独占 CPU,__do_softirq() 设置了 最大循环次数(当前为 10 次)。如果挂起位图在 10 次循环后仍未清空,内核会唤醒 ksoftirqd 内核线程在进程上下文中继续处理剩余的软中断。

2.5 为什么软中断是 tasklet 的基础

tasklet 是构建在软中断之上的更高层抽象。理解这种关系,有助于在驱动开发中正确选择使用 tasklet 还是直接使用软中断。

特性 软中断 tasklet
注册方式 静态注册,编译时固定 动态注册,运行时创建/销毁
并发性 同一软中断可在多 CPU 同时执行 同一 tasklet 串行执行,不同 tasklet 可并行
优先级 由软中断号决定(数值越小优先级越高) 仅分普通(TASKLET_SOFTIRQ)和高优先级(HI_SOFTIRQ)
实现基础 直接挂载到软中断向量表 使用 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两个软中断号
使用复杂度 高,需自行处理并发和同步 低,接口简单,自动串行化

tasklet 的底层实现机制如下:

  1. 注册阶段:系统初始化时,tasklet_action() 被注册到 TASKLET_SOFTIRQ 软中断号。
  2. 调度阶段:驱动调用 tasklet_schedule() 时,将 tasklet 加入当前 CPU 的 tasklet 链表,然后触发 TASKLET_SOFTIRQ 软中断。
  3. 执行阶段:软中断执行时调用 tasklet_action(),该函数遍历链表,逐个执行 tasklet 处理函数。
/* tasklet_schedule 的核心逻辑 */
static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
        __tasklet_schedule(t);
    }
}

void __tasklet_schedule(struct tasklet_struct *t)
{
    unsigned long flags;
    local_irq_save(flags);
    t->next = NULL;
    *__this_cpu_read(tasklet_vec.tail) = t;
    __this_cpu_write(tasklet_vec.tail, &(t->next));
    raise_softirq_irqoff(TASKLET_SOFTIRQ);
    local_irq_restore(flags);
}
核心结论:tasklet 本质上是一个 “软中断的封装层”。它利用 TASKLET_SOFTIRQ 软中断作为执行载体,通过链表管理和状态位(TASKLET_STATE_SCHED、TASKLET_STATE_RUN)实现了串行化、去重和动态调度能力。驱动开发者使用 tasklet 时,实际上是在间接使用软中断机制。

2.6 软中断与 tasklet 的选择建议

在驱动开发中,选择软中断还是 tasklet 取决于对性能和复杂度的权衡:

  • 使用软中断的场景:网络子系统、块设备层等对吞吐量要求极高,且需要精细控制并发行为的核心子系统。
  • 使用 tasklet 的场景:绝大多数设备驱动,特别是需要简单、安全的下半部处理,且对并发要求不高的场景。
  • 避免使用软中断的场景:普通驱动开发中,除非有明确的性能测量数据支持,否则应优先使用 tasklet 或 workqueue。