4、中断下半部机制概述:为什么需要下半部、下半部机制对比
在嵌入式 Linux 驱动开发中,中断处理程序(Interrupt Service Routine, ISR)必须快速执行,以释放 CPU 资源响应其他中断。然而,许多设备在中断到达后需要完成大量非紧急处理(如数据拷贝、协议解析、唤醒等待队列)。如果将这些工作全部放在 ISR 中,将导致系统中断延迟急剧增大,甚至丢失后续中断。
因此,Linux 内核将中断处理拆分为两个部分:上半部(Top Half) 和 下半部(Bottom Half)。上半部执行必须立即完成的、时间敏感的操作(如清除中断标志、保存硬件寄存器);下半部处理可以推迟的、相对耗时的任务。
4.1 为什么需要下半部
中断上下文具有以下严格限制,迫使驱动开发者将非紧急工作移出 ISR:
- 中断上下文不可睡眠: ISR 运行在中断上下文中,不能调用可能导致睡眠的函数(如
copy_to_user()、kmalloc(GFP_KERNEL)、mutex_lock())。 - 中断关闭时间必须极短: 长时间关中断会导致其他中断丢失,系统响应恶化。
- 中断嵌套风险: 高优先级中断可能打断低优先级 ISR,复杂处理容易导致栈溢出或死锁。
- 实时性要求: 实时系统要求中断延迟有确定上限,下半部机制允许调度器介入。
4.2 下半部机制对比
Linux 内核提供了四种主流下半部实现机制:Softirq、Tasklet、Workqueue 和 Threaded IRQ。它们在运行上下文、可睡眠性、并发模型和适用场景上存在显著差异。
| 特性 | Softirq | Tasklet | Workqueue | Threaded IRQ |
|---|---|---|---|---|
| 运行上下文 | 中断上下文 | 中断上下文 | 进程上下文 | 内核线程上下文 |
| 是否可睡眠 | 否 | 否 | 是 | 是 |
| 并发模型 | 同一 Softirq 可在多 CPU 同时运行 | 同一 Tasklet 串行执行,不同 CPU 可运行不同 Tasklet | 由工作队列线程调度,多 CPU 并行 | 每个 IRQ 对应一个内核线程,可设置优先级 |
| 实时性 | 高(软中断优先级高于进程) | 中(依赖软中断调度点) | 低(受进程调度影响) | 中(可设置实时优先级) |
| 使用复杂度 | 高(需静态分配,需处理重入) | 低(动态注册,自动串行化) | 中(需创建工作项和队列) | 低(request_threaded_irq 接口) |
| 典型场景 | 网络协议栈、块设备层 | 简单驱动下半部、少量数据拷贝 | 大量数据拷贝、文件系统操作、设备 I/O | 需要睡眠的复杂驱动、实时驱动 |
4.3 各机制详解
4.3.1 Softirq
Softirq 是内核中优先级最高的下半部机制,编译时静态分配。同一类型的 Softirq 可以在多个 CPU 上同时执行,因此必须处理重入问题。网络子系统(NET_TX_SOFTIRQ、NET_RX_SOFTIRQ)和块设备层是典型使用者。驱动开发者一般不直接使用 Softirq,而是通过 Tasklet 间接使用。
4.3.2 Tasklet
Tasklet 基于 Softirq 实现(使用 TASKLET_SOFTIRQ 和 HI_SOFTIRQ),但提供了更简单的接口:同一 Tasklet 在任意时刻只能在一个 CPU 上执行,不同 Tasklet 可以并行。Tasklet 是驱动开发中最常用的下半部机制,适合执行时间较短、不需要睡眠的任务。
/* Tasklet 典型使用模式 */
void my_tasklet_handler(unsigned long data)
{
/* 下半部处理:数据拷贝、状态更新、唤醒等待队列 */
struct my_device *dev = (struct my_device *)data;
dev->status = DATA_READY;
wake_up_interruptible(&dev->wq);
}
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)&my_device);
/* 在 ISR 中调度 Tasklet */
irqreturn_t my_isr(int irq, void *dev_id)
{
/* 上半部:清除中断标志,保存必要寄存器 */
writel(0, REG_INT_CLEAR);
/* 调度下半部 */
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
4.3.3 Workqueue
Workqueue 将工作推迟到内核线程中执行,运行在进程上下文,因此可以睡眠、可以持有互斥锁、可以执行文件系统操作。适用于需要大量数据拷贝、复杂协议处理或设备 I/O 的场景。Linux 提供全局工作队列 system_wq,也支持创建专用工作队列。
/* Workqueue 典型使用模式 */
struct work_struct my_work;
void my_work_handler(struct work_struct *work)
{
/* 可以睡眠,可以调用 copy_to_user */
struct my_device *dev = container_of(work, struct my_device, work);
copy_to_user(dev->buf, dev->data, dev->len);
}
/* 在 ISR 中调度 Work */
irqreturn_t my_isr(int irq, void *dev_id)
{
/* 上半部快速处理 */
schedule_work(&my_work); /* 使用系统工作队列 */
return IRQ_HANDLED;
}
4.3.4 Threaded IRQ
Threaded IRQ 是内核 2.6.30 引入的机制,通过 request_threaded_irq() 注册。它将 ISR 分为两部分:一个不可睡眠的快速处理函数(handler),和一个可睡眠的线程化处理函数(thread_fn)。线程化处理函数运行在内核线程中,可以设置实时优先级,适合对实时性有要求的驱动。
/* Threaded IRQ 典型使用模式 */
irqreturn_t my_handler(int irq, void *dev_id)
{
/* 快速上半部:清除中断标志 */
writel(0, REG_INT_CLEAR);
return IRQ_WAKE_THREAD; /* 唤醒线程化处理 */
}
irqreturn_t my_thread_fn(int irq, void *dev_id)
{
/* 线程化下半部:可以睡眠,可以持有 mutex */
struct my_device *dev = (struct my_device *)dev_id;
mutex_lock(&dev->lock);
/* 复杂处理 */
mutex_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* 注册 Threaded IRQ */
ret = request_threaded_irq(irq, my_handler, my_thread_fn,
IRQF_TRIGGER_RISING, "my_device", dev);
- 简单、快速、不需要睡眠 → Tasklet(驱动首选)
- 需要睡眠、大量数据拷贝、文件系统操作 → Workqueue
- 高性能、需要多 CPU 并行、内核子系统 → Softirq
- 需要实时优先级、需要睡眠、希望简化中断注册 → Threaded IRQ
4.4 下半部机制选择决策树
4.5 下半部调度时机
下半部并非在 ISR 返回后立即执行,而是在内核特定的调度点被处理:
- 中断返回时: 内核在
irq_exit()中检查是否有待处理的软中断,如果有则调用do_softirq()。 - ksoftirqd 内核线程: 当软中断数量过多或执行时间过长时,内核将软中断处理委托给
ksoftirqd线程,避免用户空间进程饥饿。 - 显式调用: 某些内核代码路径会主动调用
local_bh_enable()或do_softirq()来触发下半部处理。