4. 软中断基础:软中断的定义、软中断向量(softirq_vec)、软中断的触发与执行时机

4.1 软中断的定义与设计动机

软中断(SoftIRQ)是 Linux 内核中断下半部机制中最核心、最基础的实现方式。它被设计用来处理那些对实时性要求较高、但又不允许在硬中断上下文(原子上下文)中长时间执行的任务。软中断运行在软中断上下文中,允许响应硬中断,但禁止睡眠和进程切换。

核心定义: 软中断是一种静态编译时分配的、以数组形式组织的中断下半部处理机制。它通过 softirq_vec[] 全局数组管理,每个软中断对应一个处理函数,且同一类型的软中断可以在多个 CPU 上并发执行。

软中断的设计动机源于以下三个关键需求:

  • 减少硬中断屏蔽时间: 将耗时操作从硬中断处理函数中剥离,使硬中断能快速返回,提升系统实时性。
  • 提供可并发执行的下半部: 软中断天然支持 SMP(对称多处理),同一软中断可在不同 CPU 上同时运行,充分利用多核性能。
  • 保证执行顺序与优先级: 软中断具有固定的优先级(由索引号决定),高优先级的软中断优先执行,且软中断执行期间可被硬中断抢占。

4.2 软中断向量表:softirq_vec

内核使用一个全局数组 softirq_vec[] 来管理所有已注册的软中断。该数组定义在 kernel/softirq.c 中,其类型为 struct softirq_action,每个元素代表一个软中断处理描述符。

数据结构 字段 说明
struct softirq_action void (*action)(struct softirq_action *) 软中断处理函数指针,接收自身描述符作为参数
softirq_vec[NR_SOFTIRQS] 全局数组 软中断向量表,NR_SOFTIRQS 当前为 10(内核 6.x 版本)

内核预定义了 10 个软中断类型,每种类型对应一个固定的索引号。索引号越小,优先级越高。下表列出主要的软中断向量:

索引 符号常量 用途 优先级
0 HI_SOFTIRQ 高优先级 tasklet 最高
1 TIMER_SOFTIRQ 定时器(高精度与普通定时器) 高
2 NET_TX_SOFTIRQ 网络数据包发送 中高
3 NET_RX_SOFTIRQ 网络数据包接收 中高
4 BLOCK_SOFTIRQ 块设备 I/O 完成 中
5 IRQ_POLL_SOFTIRQ 中断轮询 中
6 TASKLET_SOFTIRQ 普通 tasklet 中低
7 SCHED_SOFTIRQ 调度器负载均衡 低
8 HRTIMER_SOFTIRQ 高精度定时器回调 低
9 RCU_SOFTIRQ RCU 回调处理 最低
注意: 软中断是静态分配的,内核模块无法动态注册新的软中断类型。如果需要自定义下半部,应使用 tasklet(基于软中断实现)或工作队列。

4.3 软中断的触发机制

软中断的触发通过 raise_softirq() 或 raise_softirq_irqoff() 函数完成。其核心操作是设置当前 CPU 的 irq_stat[cpu].__softirq_pending 位图中的对应位,并检查是否需要唤醒软中断守护线程 ksoftirqd。

// 触发软中断的核心流程(简化版)
void __raise_softirq_irqoff(unsigned int nr)
{
    // 1. 设置当前 CPU 的软中断 pending 位
    __this_cpu_or(irq_stat.__softirq_pending, (1UL << nr));

    // 2. 如果不在硬中断上下文且不在软中断上下文,则唤醒 ksoftirqd
    if (!in_interrupt())
        wakeup_softirqd();
}

void raise_softirq(unsigned int nr)
{
    unsigned long flags;
    local_irq_save(flags);          // 关闭本地中断
    __raise_softirq_irqoff(nr);     // 设置 pending 位
    local_irq_restore(flags);       // 恢复中断状态
}

触发时机通常发生在以下场景:

  • 硬中断处理函数末尾: 在 irq_exit() 中检查是否有待处理的软中断,并调用 invoke_softirq()。
  • 显式调用: 驱动或内核子系统在特定事件发生时主动触发(如网络驱动在接收数据包后触发 NET_RX_SOFTIRQ)。
  • ksoftirqd 守护线程: 当软中断处理时间过长或系统负载高时,内核将软中断处理委托给 ksoftirqd 内核线程。

4.4 软中断的执行时机

软中断的执行时机由内核在以下三个关键路径上检查并调度:

  1. 从硬中断返回时(irq_exit): 这是最常见的执行路径。硬中断处理完成后,irq_exit() 检查 __softirq_pending,若存在待处理软中断且不在硬中断上下文,则调用 invoke_softirq() 直接执行。
  2. 从异常/系统调用返回时: 在 do_softirq() 中,内核会在返回用户空间前检查并处理软中断,确保用户态程序不会因软中断堆积而长时间得不到响应。
  3. ksoftirqd 内核线程: 当软中断执行时间超过 MAX_SOFTIRQ_TIME(2ms)或循环次数超过 MAX_SOFTIRQ_RESTART(10次)时,内核将剩余软中断交由 ksoftirqd 线程在进程上下文中处理,避免软中断长时间占用 CPU。
关键约束: 软中断执行期间,当前 CPU 的硬中断是开启的(可被嵌套),但同一软中断类型在同一个 CPU 上不会重入。然而,不同 CPU 上可以同时执行同一个软中断,因此软中断处理函数必须使用 per-CPU 变量或自旋锁来保护共享数据。

4.5 软中断执行流程总览

下图展示了从硬中断触发到软中断执行的完整流程,以及软中断向量表的结构关系:

软中断触发与执行流程 硬中断处理 do_IRQ() / handle_irq() 触发软中断 raise_softirq(nr) 设置 pending 位 __softirq_pending |= BIT(nr) irq_exit() 检查 检查 pending 位 & 调用 invoke_softirq 软中断向量表 softirq_vec[NR_SOFTIRQS] [HI] [TIMER] [NET_TX] [NET_RX] [BLOCK] [TASKLET] [SCHED] [RCU] ... 每个元素包含 action 函数指针 执行软中断处理函数 action(softirq_action *self) 超时或超限? > 2ms 或 > 10 次循环 ksoftirqd 线程 进程上下文处理剩余软中断 软中断处理完成 软中断可被硬中断嵌套 同一 CPU 同类型不重入

4.6 软中断与硬中断的协作关系

软中断的执行时机与硬中断紧密耦合,下图展示了二者在时间轴上的协作关系:

软中断与硬中断协作时间轴 t0 t1 t2 t3 t4 硬中断处理(关中断) 软中断处理(开中断) 硬中断嵌套 ksoftirqd 关键协作规则 1. 硬中断处理期间关闭本地中断,软中断不会执行 2. 软中断执行期间开启本地中断,可被更高优先级的硬中断嵌套 3. 若软中断执行时间过长,剩余部分交由 ksoftirqd 在进程上下文完成

4.7 软中断的编程接口

内核提供以下核心 API 用于软中断的注册、触发和状态查询:

函数/宏 原型 说明
open_softirq() void open_softirq(int nr, void (*action)(struct softirq_action *)) 注册软中断处理函数,将 action 填入 softirq_vec[nr]
raise_softirq() void raise_softirq(unsigned int nr) 触发指定软中断(关中断版本,安全但稍慢)
raise_softirq_irqoff() void raise_softirq_irqoff(unsigned int nr) 触发指定软中断(调用者已关中断,性能更优)
__raise_softirq_irqoff() void __raise_softirq_irqoff(unsigned int nr) 仅设置 pending 位,不唤醒 ksoftirqd(内部使用)
local_softirq_pending() u32 local_softirq_pending(void) 返回当前 CPU 的 pending 位图
set_softirq_pending() void set_softirq_pending(u32 pending) 设置当前 CPU 的 pending 位图(慎用)
最佳实践: 在硬中断处理函数中触发软中断时,应使用 raise_softirq_irqoff(),因为此时本地中断已经关闭,无需重复调用 local_irq_save()。这可以避免不必要的指令开销,在高速网络设备驱动中尤为重要。

4.8 软中断的局限性

尽管软中断性能优异,但其设计也存在一些限制,开发者需要充分理解:

  • 静态分配: 软中断类型在编译时固定,内核模块无法动态注册新的软中断。这限制了软中断的扩展性。
  • 不支持睡眠: 软中断上下文不允许睡眠、调度或获取信号量,只能使用自旋锁或原子操作保护临界区。
  • 并发复杂性: 同一软中断可在多个 CPU 上同时执行,处理函数必须考虑 SMP 并发,增加了编程难度。
  • 执行时间敏感: 软中断执行时间过长会触发 ksoftirqd 接管,但频繁触发超时机制会影响系统实时性。
适用场景: 软中断最适合处理高频、短小、对吞吐量要求极高的任务,典型代表是网络数据包收发(NET_TX/NET_RX)和块设备 I/O 完成(BLOCK)。对于大多数驱动开发者,更推荐使用基于软中断实现的 tasklet 或工作队列。