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 软中断的注册流程
软中断的注册在系统启动早期完成,典型调用链如下:
- 定义软中断号:在
<linux/interrupt.h>中枚举,如HI_SOFTIRQ、TASKLET_SOFTIRQ、NET_TX_SOFTIRQ等。 - 编写处理函数:函数原型为
void (*action)(struct softirq_action *)。 - 调用
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 的挂起位图,依次执行已触发的软中断处理函数。执行流程如下图所示:
重要限制:为防止软中断独占 CPU,
__do_softirq() 设置了 最大循环次数(当前为 10 次)。如果挂起位图在 10 次循环后仍未清空,内核会唤醒 ksoftirqd 内核线程在进程上下文中继续处理剩余的软中断。
2.5 为什么软中断是 tasklet 的基础
tasklet 是构建在软中断之上的更高层抽象。理解这种关系,有助于在驱动开发中正确选择使用 tasklet 还是直接使用软中断。
| 特性 | 软中断 | tasklet |
|---|---|---|
| 注册方式 | 静态注册,编译时固定 | 动态注册,运行时创建/销毁 |
| 并发性 | 同一软中断可在多 CPU 同时执行 | 同一 tasklet 串行执行,不同 tasklet 可并行 |
| 优先级 | 由软中断号决定(数值越小优先级越高) | 仅分普通(TASKLET_SOFTIRQ)和高优先级(HI_SOFTIRQ) |
| 实现基础 | 直接挂载到软中断向量表 | 使用 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两个软中断号 |
| 使用复杂度 | 高,需自行处理并发和同步 | 低,接口简单,自动串行化 |
tasklet 的底层实现机制如下:
- 注册阶段:系统初始化时,
tasklet_action()被注册到TASKLET_SOFTIRQ软中断号。 - 调度阶段:驱动调用
tasklet_schedule()时,将 tasklet 加入当前 CPU 的 tasklet 链表,然后触发TASKLET_SOFTIRQ软中断。 - 执行阶段:软中断执行时调用
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。