一、为什么需要软中断?
要理解软中断,首先要明确硬中断的局限性:
- 硬中断执行时会关闭本地 CPU 的中断(至少关闭对应中断线),如果硬中断处理函数耗时过长,会导致其他硬件中断无法及时响应,甚至丢失中断(比如网卡丢包)。
- 硬中断是硬件触发的,执行上下文是中断上下文,不能睡眠、不能调度,只能处理最紧急的硬件操作。
- 硬中断无法并行:同一个中断线的硬中断不能嵌套,即使在多核 CPU 上,同一个硬中断处理函数也只能串行执行。
因此,内核将中断处理拆分为两个阶段:
- 上半部(Top Half):由硬中断处理,只做最紧急、最快速的操作(比如读取网卡寄存器、把数据从硬件拷贝到内核 Ring Buffer、标记中断状态),然后触发软中断,立即返回。
- 下半部(Bottom Half):由软中断执行,处理耗时但不紧急的操作(比如解析网络包、协议栈处理、数据拷贝到用户态),执行时开中断,可以被硬中断抢占,多核 CPU 上还能并行执行。
二、软中断的核心特性
1. 与硬中断的核心区别
| 特性 | 硬中断 | 软中断 |
|---|---|---|
| 触发方式 | 硬件设备主动触发(通过中断控制器) | 内核代码主动触发(raise_softirq()) |
| 执行时机 | 硬件中断发生后立即执行 | 硬中断返回时、ksoftirqd 线程中、显式调度时 |
| 中断状态 | 执行时关闭本地对应中断 | 执行时开启所有中断 |
| 执行上下文 | 中断上下文,不能睡眠、不能调度 | 中断上下文,不能睡眠、不能调度 |
| 并发性 | 不可嵌套,同中断线串行执行 | 多核 CPU 上可并行执行(不同 CPU) |
| 优先级 | 最高 | 低于硬中断,高于普通进程 |
2. 软中断的本质
软中断本质是内核预定义的一组全局函数指针数组,每个数组元素对应一个软中断类型,包含该软中断的处理函数。内核通过pending 位图标记哪些软中断需要执行,当触发软中断时,只需要将对应位图位置 1,内核会在合适的时机执行对应的处理函数。
三、软中断的内核实现机制
1. 软中断的定义与注册
Linux 内核中,软中断的最大数量固定为 32 个(目前只使用了前 10 个),通过softirq_vec数组管理,定义如下:
struct softirq_action { void (*action)(struct softirq_action *); // 软中断处理函数 }; struct softirq_action softirq_vec[32];内核启动时,通过open_softirq()函数注册不同类型软中断的处理函数,例如网络接收软中断的注册:
open_softirq(NET_RX_SOFTIRQ, net_rx_action); // 注册网络接收软中断 open_softirq(NET_TX_SOFTIRQ, net_tx_action); // 注册网络发送软中断2. 常见的软中断类型
内核预定义的软中断类型(优先级从高到低):
| 软中断类型 | 数值 | 用途 |
|---|---|---|
HI_SOFTIRQ | 0 | 高优先级 tasklet(处理紧急的小任务) |
TIMER_SOFTIRQ | 1 | 内核定时器(处理超时事件) |
NET_TX_SOFTIRQ | 2 | 网络数据包发送 |
NET_RX_SOFTIRQ | 3 | 网络数据包接收(最常用的软中断之一) |
BLOCK_SOFTIRQ | 4 | 块设备 I/O 处理(磁盘、SSD 等) |
IRQ_POLL_SOFTIRQ | 5 | 中断轮询(替代传统硬中断的轮询机制) |
TASKLET_SOFTIRQ | 6 | 普通 tasklet(驱动开发常用) |
SCHED_SOFTIRQ | 7 | 进程调度(负载均衡、唤醒进程) |
HRTIMER_SOFTIRQ | 8 | 高精度定时器 |
RCU_SOFTIRQ | 9 | RCU 锁的延迟回收(内核同步机制) |
3. 软中断的触发
软中断通过raise_softirq()函数触发,核心操作是将对应软中断的 pending 位图位置 1。例如,网卡硬中断处理函数中触发网络接收软中断:
raise_softirq(NET_RX_SOFTIRQ);注意:如果触发软中断时,当前 CPU 正在处理硬中断,软中断不会立即执行,而是等待硬中断返回后再执行。
4. 软中断的执行时机
内核会在以下 3 个关键时机检查并执行 pending 的软中断:
- 硬中断处理完成后:这是最常见的执行时机,硬中断返回用户态 / 内核态前,会调用
do_softirq()处理所有 pending 的软中断。 - 内核线程 ksoftirqd 中:如果软中断执行时间过长(比如网络突发大量数据包),内核会唤醒每个 CPU 对应的
ksoftirqd/n内核线程,专门处理软中断,避免软中断占满 CPU 导致用户进程饿死。 - 显式调用
local_bh_enable()时:当内核代码主动开启下半部时,会检查并执行 pending 的软中断。
5. 软中断的处理流程
以网络包接收为例,完整的软中断处理流程如下:
- 网卡收到数据包,通过 DMA 将数据拷贝到内核的 Ring Buffer,触发硬中断。
- 网卡硬中断处理函数执行:
- 读取网卡状态,确认是接收中断。
- 关闭网卡接收中断(NAPI 机制),避免持续触发硬中断。
- 调用
raise_softirq(NET_RX_SOFTIRQ),标记网络接收软中断为 pending。 - 硬中断处理完成,返回。
- 硬中断返回前,内核调用
do_softirq():- 遍历 pending 位图,找到需要执行的软中断(NET_RX_SOFTIRQ)。
- 调用对应的处理函数
net_rx_action()。 net_rx_action()从 Ring Buffer 中批量读取数据包,封装为sk_buff结构体,交给 TCP/IP 协议栈逐层处理(IP 层→TCP 层→Socket 层)。- 处理完成后,清除 pending 位图,开启网卡接收中断(NAPI 机制)。
- 如果
net_rx_action()处理超时(默认最多处理 2ms),剩余的数据包会交给ksoftirqd内核线程继续处理。
四、软中断的衍生机制:Tasklet
软中断虽然性能高,但存在两个问题:
- 同一个软中断的处理函数可以在多个 CPU 上并行执行,驱动开发者需要自行处理多核并发问题,容易出错。
- 软中断的类型是内核预定义的,驱动不能动态添加新的软中断类型。
为了解决这些问题,内核基于TASKLET_SOFTIRQ和HI_SOFTIRQ封装了Tasklet 机制,是驱动开发中最常用的下半部方式。
1. Tasklet 的核心特性
- 串行执行:同一个 Tasklet 只会在一个 CPU 上执行,不会在多个 CPU 上并行运行,无需考虑多核并发。
- 动态创建:驱动可以随时创建和销毁 Tasklet,无需修改内核代码。
- 优先级区分:高优先级 Tasklet 基于
HI_SOFTIRQ,普通 Tasklet 基于TASKLET_SOFTIRQ。
2. Tasklet 的使用示例
// 定义Tasklet处理函数 void my_tasklet_handler(unsigned long data) { // 处理耗时但不紧急的操作(不能睡眠) printk("Tasklet executed, data: %ld\n", data); } // 声明并初始化Tasklet DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 123); // 硬中断处理函数中触发Tasklet irqreturn_t my_interrupt(int irq, void *dev_id) { // 上半部:快速处理硬件操作 tasklet_schedule(&my_tasklet); // 触发Tasklet return IRQ_HANDLED; }五、软中断 vs Tasklet vs 工作队列
内核中常用的延迟执行机制有三种:软中断、Tasklet、工作队列,它们的适用场景完全不同:
表格
| 特性 | 软中断 | Tasklet | 工作队列 |
|---|---|---|---|
| 执行上下文 | 中断上下文 | 中断上下文 | 进程上下文 |
| 能否睡眠 | 不能 | 不能 | 可以(睡眠、调度) |
| 并发性 | 多核可并行 | 同 Tasklet 串行 | 多核可并行 |
| 实时性 | 最高 | 高 | 低 |
| 适用场景 | 内核核心子系统(网络、块设备) | 驱动开发(简单延迟任务) | 需要睡眠的延迟任务 |
| 开发难度 | 高(需处理并发) | 低(无需处理并发) | 低(和普通进程一致) |
六、软中断的性能优化与常见问题
1. 软中断 CPU 占用过高
当系统出现软中断 CPU 占比过高(通过top命令查看si指标)时,通常是以下原因:
- 网络突发流量:大量网络包导致
NET_RX_SOFTIRQ持续执行,此时需要优化网络协议栈、开启 RPS/RFS(多 CPU 分发网络包)。 - 块设备 I/O 密集:大量磁盘读写导致
BLOCK_SOFTIRQ占用过高,此时需要优化 I/O 调度算法、使用 SSD。 - 软中断执行时间过长:软中断处理函数中存在耗时操作,需要进一步拆分任务,交给工作队列处理。
2. ksoftirqd 内核线程的作用
每个 CPU 对应一个ksoftirqd/n内核线程,当软中断的执行时间超过阈值(默认 2ms),或者软中断被连续触发时,内核会将剩余的软中断交给ksoftirqd处理。这样可以避免软中断长时间占用 CPU,导致用户进程无法得到调度。
3. NAPI 机制与软中断的结合
NAPI(New API)是 Linux 网络子系统的核心优化,它将中断 + 轮询结合,大幅减少了硬中断的触发次数:
- 当网卡收到第一个包时,触发硬中断,关闭网卡中断,触发
NET_RX_SOFTIRQ。 - 软中断处理函数
net_rx_action()轮询 Ring Buffer,批量处理所有已到达的数据包。 - 处理完成后,重新开启网卡中断,等待下一个包的到来。
这种方式避免了每个数据包都触发一次硬中断,在高并发网络场景下性能提升显著。
七、总结
软中断是 Linux 内核实现高性能异步处理的核心机制,它通过拆分中断处理的上下半部,解决了硬中断耗时过长的问题。其核心要点:
- 软中断是内核态、不可睡眠、可并行的延迟执行机制,优先级高于普通进程。
- 网络、块设备、定时器等内核核心子系统都依赖软中断实现高性能处理。
- Tasklet 是软中断的封装,简化了驱动开发;工作队列则用于需要睡眠的延迟任务。
- 软中断的性能瓶颈通常出现在高并发网络或 I/O 场景,可通过 NAPI、多 CPU 分发、硬件加速等方式优化。