2. Linux 中断处理机制:上半部(Top Half)与下半部(Bottom Half)机制详解
在 Linux 内核中,中断处理被明确划分为两个阶段:上半部(Top Half) 和 下半部(Bottom Half)。这种设计是为了在中断响应速度和系统整体性能之间取得平衡。上半部在关闭中断或屏蔽特定中断线的上下文中执行,要求极短的处理时间;而下半部则在中断重新使能后,以更宽松的调度条件完成剩余工作。
核心原则: 上半部只做最必要、最紧急的硬件操作(如清除中断标志、拷贝硬件寄存器数据),将耗时、可延迟的处理任务(如协议解析、数据分发)推迟到下半部执行。
2.1 上半部(Top Half)—— 硬中断上下文
上半部是中断请求(IRQ)触发后立即执行的代码路径。它运行在硬中断上下文中,具有以下特征:
- 不可睡眠: 不能调用可能导致进程睡眠的函数(如
kmalloc(GFP_KERNEL)、mutex_lock())。 - 不可重入: 对于同一中断线,上半部执行期间该中断线被屏蔽,避免嵌套。
- 执行时间极短: 通常只进行几十到几百微秒的操作,否则会严重影响系统实时性。
- 优先级最高: 任何进程上下文都会被抢占。
典型上半部工作: 读取硬件状态寄存器、保存关键数据到内存缓冲区、清除中断挂起位、调度下半部。
2.2 下半部(Bottom Half)—— 可延迟处理机制
下半部是上半部调度执行的、可延迟的处理任务。Linux 内核提供了三种主要的软中断机制来实现下半部:软中断(Softirq)、tasklet 和 工作队列(Workqueue)。它们在不同的上下文和调度约束下运行。
| 特性 | 软中断 (Softirq) | Tasklet | 工作队列 (Workqueue) |
|---|---|---|---|
| 执行上下文 | 软中断上下文(中断上下文) | 软中断上下文(中断上下文) | 进程上下文(内核线程) |
| 可睡眠 | 否 | 否 | 是 |
| 可重入 | 是(同一类型可在不同 CPU 上同时运行) | 否(同一 tasklet 串行执行) | 是(由内核线程池管理) |
| 调度方式 | 编译时静态分配,通过 raise_softirq() 激活 |
动态注册,通过 tasklet_schedule() 调度 |
动态创建,通过 queue_work() 提交 |
| 适用场景 | 高性能、频繁触发、对延迟敏感(如网络 RX/TX) | 简单、串行化的下半部任务(如块设备层) | 需要睡眠、持有锁、执行文件系统操作等复杂任务 |
性能陷阱: 软中断和 tasklet 运行在中断上下文中,不能睡眠。如果处理时间过长,会导致用户空间进程饥饿,并增加中断延迟。工作队列虽然灵活,但上下文切换开销较大,不适合极高频的轻量级任务。
2.3 软中断(Softirq)机制详解
软中断是 Linux 内核中优先级最高、开销最小的下半部机制。它在内核编译时静态分配,数量有限(目前约 10 种,如 NET_TX_SOFTIRQ、TASKLET_SOFTIRQ)。
- 注册: 通过
open_softirq()将处理函数绑定到软中断号。 - 触发: 上半部调用
raise_softirq()或raise_softirq_irqoff()标记软中断为待处理。 - 执行: 在
do_softirq()中遍历待处理软中断向量表,调用对应的处理函数。软中断会在以下时机被检查并执行:- 硬中断处理程序退出时(
irq_exit())。 - 由
ksoftirqd内核线程在进程上下文中处理(当软中断堆积过多时)。
- 硬中断处理程序退出时(
关键优化点: 软中断可以在多个 CPU 上并行执行同一类型的处理函数,因此必须使用 per-CPU 变量或 RCU 等机制保护数据,避免锁竞争。
2.4 Tasklet 机制详解
Tasklet 是建立在软中断之上的、更易用的下半部接口。它本质上是对 TASKLET_SOFTIRQ 软中断的封装。与软中断相比,tasklet 具有以下特点:
- 串行化执行: 同一类型的 tasklet 在同一时刻只能在一个 CPU 上执行,避免了复杂的并发保护。
- 动态注册: 驱动可以随时创建和销毁 tasklet。
- 简单 API: 使用
DECLARE_TASKLET()或tasklet_init()初始化,tasklet_schedule()调度执行。
/* Tasklet 使用示例 */
#include <linux/interrupt.h>
/* 定义 tasklet 处理函数 */
void my_tasklet_handler(unsigned long data)
{
/* 执行下半部工作,不可睡眠 */
printk(KERN_INFO "Tasklet executed, data=%lu\n", data);
}
/* 静态声明 tasklet */
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);
/* 上半部中断处理函数 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
/* 调度 tasklet 执行 */
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
使用建议: 对于大多数设备驱动,tasklet 是下半部的首选机制。它平衡了性能与易用性。仅在需要极高吞吐量且能精细控制并发的场景(如网络子系统)才直接使用软中断。
2.5 工作队列(Workqueue)机制详解
工作队列将下半部任务交给内核线程在进程上下文中执行。这带来了最大的灵活性:
- 可睡眠: 可以调用
kmalloc(GFP_KERNEL)、mutex_lock()、copy_from_user()等可能阻塞的函数。 - 可管理: 工作项可以被取消、延迟、排队到特定的工作队列(如系统默认的
system_wq或自定义的 per-CPU 队列)。 - 上下文切换开销: 相比软中断和 tasklet,工作队列的调度延迟更高,不适合微秒级的高频任务。
/* 工作队列使用示例 */
#include <linux/workqueue.h>
/* 定义工作项处理函数 */
void my_work_handler(struct work_struct *work)
{
/* 执行下半部工作,可以睡眠 */
msleep(10);
printk(KERN_INFO "Workqueue handler executed\n");
}
/* 声明工作项 */
DECLARE_WORK(my_work, my_work_handler);
/* 上半部中断处理函数 */
irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
/* 将工作项提交到系统默认工作队列 */
schedule_work(&my_work);
return IRQ_HANDLED;
}
2.6 中断处理流程总结
一个完整的中断处理生命周期可以概括为以下步骤:
- 硬件中断触发: CPU 接收到中断信号,保存当前上下文,跳转到中断向量表。
- 上半部执行: 调用注册的
irq_handler_t,在关闭中断的情况下执行紧急操作。 - 调度下半部: 上半部通过
raise_softirq()、tasklet_schedule()或schedule_work()标记或提交下半部任务。 - 中断退出: 恢复硬件中断使能,检查是否有待处理的软中断。
- 下半部执行: 在合适的时机(软中断上下文或进程上下文)执行下半部处理函数。
性能优化关键: 上半部应尽可能短,只做“记录”和“调度”两件事。下半部的选择直接影响系统的吞吐量和调度延迟。对于中断负载均衡而言,理解这三种机制的上下文切换成本和并发模型,是后续配置 SMP IRQ Affinity 和 RPS/RFS 的基础。