4. Linux中断处理架构
在实时系统中,中断响应延迟是衡量系统实时性能的核心指标。Linux 内核为了在中断响应速度与系统整体吞吐量之间取得平衡,将中断处理过程拆分为两个阶段:上半部(Top Half) 与 下半部(Bottom Half)。这种架构设计是理解中断线程化改造的基础。
4.1 上半部(Top Half)与下半部(Bottom Half)的设计哲学
中断处理程序执行时,CPU 会屏蔽同级或更低级的中断,导致系统无法响应其他中断事件。如果中断处理程序执行时间过长,将严重影响系统的实时性和吞吐量。因此,内核将中断处理拆分为两部分:
- 上半部(Top Half):在中断上下文中立即执行,要求快速、简短、不可阻塞。通常只完成最紧急的硬件操作,如清除中断标志、拷贝硬件寄存器数据等。
- 下半部(Bottom Half):推迟到稍后时间执行,运行在软中断上下文或进程上下文中,可以处理更复杂的任务,如协议栈处理、数据拷贝等。
关键原则:上半部执行时间应控制在微秒级,仅完成必须立即响应的硬件操作;其余所有可推迟的工作全部交由下半部处理。
实时性启示:上半部执行期间中断被屏蔽,这是导致中断延迟抖动的核心来源。中断线程化的目标就是将上半部的屏蔽时间压缩到极致,甚至将整个上半部线程化。
4.2 下半部机制演进:从软中断到工作队列
Linux 内核提供了多种下半部实现机制,它们在执行上下文、可阻塞性、实时性方面存在显著差异。下表总结了三种主要机制:
| 特性 | 软中断(SoftIRQ) | Tasklet | 工作队列(Workqueue) |
|---|---|---|---|
| 执行上下文 | 软中断上下文(原子) | 软中断上下文(原子) | 进程上下文(可睡眠) |
| 可阻塞/睡眠 | 否 | 否 | 是 |
| 并发性 | 同一类型可在多核并行 | 同一类型串行执行 | 由 worker pool 管理,可并行 |
| 实时性影响 | 高优先级,但不可抢占 | 中等,不可抢占 | 低优先级,可被高优先级任务抢占 |
| 典型用途 | 网络 RX/TX、定时器 | 驱动下半部、简单推迟任务 | 复杂、可阻塞的推迟任务 |
4.3 软中断(SoftIRQ)机制详解
软中断是内核中优先级最高的下半部机制,直接由 __do_softirq() 函数在中断返回路径上执行。软中断在编译时静态分配,系统定义了有限的软中断类型(如 NET_RX_SOFTIRQ、TASKLET_SOFTIRQ 等)。
// 软中断类型定义(kernel/softirq.c)
enum {
HI_SOFTIRQ = 0, // 高优先级 tasklet
TIMER_SOFTIRQ, // 定时器
NET_TX_SOFTIRQ, // 网络发送
NET_RX_SOFTIRQ, // 网络接收
BLOCK_SOFTIRQ, // 块设备
IRQ_POLL_SOFTIRQ, // 中断轮询
TASKLET_SOFTIRQ, // 普通 tasklet
SCHED_SOFTIRQ, // 调度器
HRTIMER_SOFTIRQ, // 高精度定时器
RCU_SOFTIRQ, // RCU 处理
NR_SOFTIRQS
};
软中断的执行具有以下特点:
- 在
irq_exit()中被调用,运行在中断上下文中。 - 同一类型的软中断可以在多个 CPU 上同时执行,但必须保证数据结构的并发安全。
- 软中断执行期间,可以被更高优先级的中断打断,但不能被其他软中断抢占。
实时性风险:软中断在中断上下文中执行,不可被实时任务抢占。如果网络软中断处理时间过长(如处理大量数据包),将导致实时任务错过截止时间。这是 PREEMPT_RT 补丁需要将软中断线程化的核心原因之一。
4.4 Tasklet 机制:基于软中断的简化封装
Tasklet 是建立在软中断之上的更易用的下半部接口。它使用 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两个软中断类型,通过链表管理多个 tasklet 实例。
// Tasklet 使用示例
#include <linux/interrupt.h>
// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data) {
// 处理推迟的工作,不可睡眠
printk("Tasklet executed on CPU %d\n", smp_processor_id());
}
// 声明并初始化 tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);
// 在中断上半部中调度 tasklet
irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
// 快速硬件操作
// ...
// 调度下半部
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
Tasklet 与软中断的关键区别:
- 串行性:同一 tasklet 实例在任何时刻只能在一个 CPU 上执行,避免了并发问题。
- 动态性:tasklet 可以在模块加载时动态创建和注册。
- 优先级:
HI_SOFTIRQ类型的 tasklet 优先级高于普通 tasklet。
4.5 工作队列(Workqueue)机制:进程上下文的下半部
工作队列将下半部任务推迟到内核线程中执行,运行在进程上下文中,因此可以睡眠、持有信号量、执行可能阻塞的 I/O 操作。工作队列是唯一可以在下半部中安全使用阻塞操作的内核机制。
// 工作队列使用示例
#include <linux/workqueue.h>
// 定义工作处理函数
void my_work_handler(struct work_struct *work) {
// 可以睡眠,可以持有 mutex
msleep(10);
printk("Work executed in process context\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;
}
工作队列的演进:
- 传统工作队列:每个 CPU 一个
events/n内核线程,所有工作项共享该线程。 - CMWQ(Concurrency Managed Workqueue):Linux 3.x 引入,使用 worker pool 动态管理线程数量,避免线程过多导致的上下文切换开销。
4.6 中断处理全流程架构图
下图展示了从硬件中断触发到上半部、下半部处理的完整流程:
4.7 下半部选择决策指南
在实际驱动开发中,选择哪种下半部机制取决于任务的特性和实时性要求:
- 任务是否可以睡眠?
- 是 → 使用工作队列(Workqueue)
- 否 → 考虑软中断或 Tasklet
- 是否需要多核并行执行?
- 是 → 使用软中断(需自行处理并发)
- 否 → 使用 Tasklet(自动串行化)
- 实时性要求是否苛刻?
- 是 → 避免使用软中断,优先使用工作队列(可被抢占)
- 否 → 根据性能需求选择
工程实践建议:在实时系统中,应尽量避免在软中断和 Tasklet 中执行耗时操作。对于网络驱动等高性能场景,可考虑使用 NAPI(New API)结合软中断,但需配合中断节流(Interrupt Coalescing)技术控制处理时间。
4.8 中断线程化的前置理解
理解上半部与下半部的划分机制,是掌握中断线程化的前提。中断线程化的核心思想是:
- 将原本在中断上下文中执行的上半部代码,移入内核线程中执行。
- 将软中断和 Tasklet 也线程化,使其运行在可被实时任务抢占的进程上下文中。
- 通过实时调度策略(如 SCHED_FIFO)控制中断处理线程的优先级,确保关键中断的实时响应。