4. 中断线程化原理:将中断处理程序下半部移至内核线程,使中断可被调度与抢占
中断线程化(Interrupt Threading)是实时操作系统与嵌入式 Linux 中解决“中断不可抢占”问题的核心手段。其基本思想是将中断处理程序分为两部分:上半部(Hard IRQ Context) 执行必须关中断的极简操作(如清中断标志、拷贝硬件寄存器),下半部(Threaded IRQ) 则作为一个可被调度、可被抢占的内核线程运行,允许其他高优先级任务或实时线程在中断处理期间获得 CPU 控制权。
4.1 中断处理的分段模型
传统中断处理模型将全部逻辑放在关中断上下文中执行,导致系统对中断的响应延迟不可控。中断线程化通过引入“硬中断上下文 + 内核线程上下文”的两段模型,将可延迟执行的工作剥离出来。
| 阶段 | 执行上下文 | 可抢占性 | 典型操作 |
|---|---|---|---|
| 上半部(Top Half) | 硬中断上下文(IRQ off) | 不可抢占,关中断 | 读取 FIFO、清中断挂起、唤醒线程 |
| 下半部(Bottom Half) | 内核线程上下文(可睡眠) | 可被调度与抢占 | 协议解析、数据拷贝、通知用户态 |
4.2 中断线程化的调度与抢占机制
当中断发生时,硬件自动关闭本地 CPU 中断,执行上半部。上半部结束后,内核将对应的线程化中断处理函数挂入内核线程的调度队列。该线程拥有独立的 task_struct,可以被调度器按优先级调度,也可以被更高优先级的实时任务或中断线程抢占。
4.3 线程化中断的优先级与抢占规则
每个线程化中断都对应一个内核线程,其优先级可通过 request_threaded_irq() 或 irq_set_affinity_hint() 配合调度策略进行设置。抢占规则遵循标准 Linux 实时调度策略(SCHED_FIFO / SCHED_RR):
- 优先级比较:线程化中断线程的优先级通常设置为高于普通任务但低于硬实时任务,确保中断处理不被无限阻塞。
- 可抢占条件:当更高优先级的任务或中断线程就绪时,当前正在执行的线程化下半部会被立即抢占,保存上下文后让出 CPU。
- 优先级反转防护:线程化中断线程在持有自旋锁或互斥锁时,若被低优先级任务阻塞,可通过优先级继承协议(Priority Inheritance)临时提升阻塞任务的优先级。
4.4 线程化中断的注册与实现接口
Linux 内核从 2.6.30 开始提供 request_threaded_irq() 接口,允许驱动开发者显式指定上半部与下半部处理函数。若上半部为 NULL,内核会自动使用 irq_default_primary_handler() 快速唤醒线程。
/* 线程化中断注册示例 */
int request_threaded_irq(unsigned int irq,
irq_handler_t handler, /* 上半部,可为 NULL */
irq_handler_t thread_fn, /* 线程化下半部 */
unsigned long irqflags,
const char *devname,
void *dev_id);
/* 实际驱动使用模式 */
static irqreturn_t my_irq_top_half(int irq, void *dev_id)
{
/* 只做:读状态寄存器、清中断标志、返回 IRQ_WAKE_THREAD */
return IRQ_WAKE_THREAD;
}
static irqreturn_t my_irq_bottom_half(int irq, void *dev_id)
{
/* 可睡眠、可被抢占:协议解析、数据拷贝、唤醒等待队列 */
struct my_dev *dev = (struct my_dev *)dev_id;
spin_lock(&dev->lock);
/* 处理数据 */
spin_unlock(&dev->lock);
wake_up_interruptible(&dev->wq);
return IRQ_HANDLED;
}
/* 注册:上半部 my_irq_top_half,下半部 my_irq_bottom_half */
ret = request_threaded_irq(irq_num, my_irq_top_half,
my_irq_bottom_half,
IRQF_TRIGGER_RISING,
"my_driver", dev);
IRQ_WAKE_THREAD,内核才会唤醒线程化下半部;若返回 IRQ_HANDLED,则不会触发线程。这是驱动开发者最容易遗漏的细节。
4.5 中断线程化 vs 传统下半部机制对比
| 特性 | 传统 SoftIRQ / Tasklet | 线程化中断(Threaded IRQ) |
|---|---|---|
| 执行上下文 | 软中断上下文(不可睡眠) | 内核线程上下文(可睡眠) |
| 可抢占性 | 不可被任务抢占(仅在 ksoftirqd 中可被中断) | 完全可被调度器抢占 |
| 优先级控制 | 固定,无法单独设置 | 可设置 SCHED_FIFO 优先级 |
| 最大关中断时间 | 受 SoftIRQ 执行时间影响 | 仅受上半部影响,通常 < 5μs |
| 适用场景 | 高吞吐、低延迟网络/存储 | 实时控制、工业通信、多媒体 |
4.6 中断线程化在实时内核(PREEMPT_RT)中的角色
在 PREEMPT_RT 内核中,中断线程化被提升为默认行为:所有中断(包括定时器中断)默认都运行在线程化上下文中。这使得系统几乎完全消除了“不可抢占”的中断上下文,实时任务可以获得极低且确定的调度延迟。
- 强制线程化:通过内核启动参数
threadirqs或PREEMPT_RT全量开启,所有 IRQ 都变为线程化。 - 自旋锁转换:线程化中断中不再使用普通自旋锁,而是使用
spin_lock_irqsave或 RT 专用的raw_spin_lock,避免优先级反转。 - 性能权衡:线程化增加了上下文切换开销,但在实时性要求高的场景下,确定性比吞吐量更重要。
4.7 中断线程化移植的关键步骤
- 识别可线程化的中断:分析现有驱动,将非原子操作(如数据拷贝、协议解析、I2C/SPI 通信)从上半部剥离。
- 拆分处理函数:上半部只保留寄存器读写、中断标志清除、唤醒线程;下半部实现全部业务逻辑。
- 替换注册接口:将
request_irq()改为request_threaded_irq(),并确保上半部返回IRQ_WAKE_THREAD。 - 锁机制适配:线程化下半部中可使用
mutex或spin_lock_irqsave(取决于是否与上半部共享数据),避免在关中断上下文中使用睡眠锁。 - 优先级调优:通过
sched_setscheduler()或irq_set_affinity_hint()设置线程化中断的实时优先级,确保关键中断不被饿死。 - 验证关中断时间:使用
ftrace或trace_preemptirqsoff测量最大关中断时间,确认优化效果。
- 上半部是否只包含必须关中断的操作?
- 下半部是否使用了可睡眠的锁或 API?
- 线程化中断的优先级是否高于所有可能阻塞它的普通任务?
- 是否已测量并记录移植前后的最大关中断时间?
中断线程化是嵌入式实时系统从“裸机中断思维”转向“可抢占内核思维”的关键一步。掌握其原理与移植方法,是后续优化抢占临界区、实现微秒级响应的重要基础。