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;
}
注册流程的关键步骤:
- 分配软中断编号:使用预定义的枚举值(如 TASKLET_SOFTIRQ)。
- 编写处理函数:函数原型为
void action(struct softirq_action *)。 - 调用 open_softirq():在系统初始化或模块加载时注册。
- 激活软中断:通过
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 架构图
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 竞态条件的第一道防线。