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 软中断的执行时机
软中断的执行时机由内核在以下三个关键路径上检查并调度:
- 从硬中断返回时(irq_exit): 这是最常见的执行路径。硬中断处理完成后,
irq_exit()检查__softirq_pending,若存在待处理软中断且不在硬中断上下文,则调用invoke_softirq()直接执行。 - 从异常/系统调用返回时: 在
do_softirq()中,内核会在返回用户空间前检查并处理软中断,确保用户态程序不会因软中断堆积而长时间得不到响应。 - ksoftirqd 内核线程: 当软中断执行时间超过
MAX_SOFTIRQ_TIME(2ms)或循环次数超过MAX_SOFTIRQ_RESTART(10次)时,内核将剩余软中断交由ksoftirqd线程在进程上下文中处理,避免软中断长时间占用 CPU。
关键约束: 软中断执行期间,当前 CPU 的硬中断是开启的(可被嵌套),但同一软中断类型在同一个 CPU 上不会重入。然而,不同 CPU 上可以同时执行同一个软中断,因此软中断处理函数必须使用 per-CPU 变量或自旋锁来保护共享数据。
4.5 软中断执行流程总览
下图展示了从硬中断触发到软中断执行的完整流程,以及软中断向量表的结构关系:
4.6 软中断与硬中断的协作关系
软中断的执行时机与硬中断紧密耦合,下图展示了二者在时间轴上的协作关系:
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 或工作队列。