中断线程化原理:从硬中断上下文到内核线程上下文的迁移
中断线程化(IRQ Threading)是 PREEMPT_RT 内核实现实时化的核心机制之一。其本质是将传统 Linux 内核中在硬中断上下文(Hard IRQ Context)执行的 中断处理程序(Interrupt Handler),拆分为两个阶段:上半部(Top Half) 和 下半部(Bottom Half),并将下半部迁移到一个可被实时调度器管理的内核线程上下文中执行。
2.1 传统中断处理模型的局限性
在标准 Linux 内核中,中断处理程序直接运行在硬中断上下文中,具有以下特点:
- 不可抢占:硬中断上下文会屏蔽当前 CPU 上的本地中断,直到处理程序返回。
- 优先级反转:即使高优先级的实时任务就绪,也无法抢占正在执行的中断处理程序。
- 不确定的延迟:中断处理程序的执行时间直接叠加到任务调度延迟中,破坏实时确定性。
关键问题: 传统模型中,一个简单的网卡中断处理程序可能执行 50-100 µs,在此期间所有高优先级实时任务都被阻塞,导致最坏情况下的调度延迟不可控。
2.2 中断线程化的核心流程
中断线程化将中断处理程序拆分为两个阶段,并通过 request_threaded_irq() API 注册:
- 硬中断处理函数(handler):在真正的硬中断上下文中执行,必须极短(通常仅做硬件确认、清除中断标志、唤醒线程)。
- 线程化处理函数(thread_fn):在一个内核线程(irq/xxx-name)中执行,该线程受内核调度器管理,可被抢占,支持优先级和调度策略设置。
// 中断线程化注册示例
int request_threaded_irq(unsigned int irq,
irq_handler_t handler, // 硬中断上半部
irq_handler_t thread_fn, // 线程化下半部
unsigned long irqflags,
const char *devname,
void *dev_id);
// 实际驱动调用示例
ret = request_threaded_irq(irq_num,
my_irq_handler, // 快速硬件处理
my_irq_thread_fn, // 线程化处理
IRQF_TRIGGER_RISING,
"my_device",
dev);
API 要点: 如果
handler 返回 IRQ_WAKE_THREAD,内核会唤醒对应的线程化处理函数;如果返回 IRQ_HANDLED,则不会唤醒线程。驱动开发者必须正确设置返回值。
2.3 上下文迁移的完整过程
下图展示了从硬件中断触发到线程化处理函数执行的完整上下文迁移路径:
2.4 上下文对比:硬中断 vs 线程化中断
| 对比维度 | 硬中断上下文 | 线程化中断上下文 |
|---|---|---|
| 执行环境 | 中断栈(通常 4KB-16KB) | 内核线程栈(通常 8KB) |
| 可抢占性 | 不可抢占(中断关闭) | 可被高优先级任务抢占 |
| 调度策略 | 无调度(立即执行) | 支持 SCHED_FIFO / SCHED_RR |
| 优先级 | 固定高于所有进程 | 可配置(默认 50) |
| 允许操作 | 仅原子操作、spinlock | mutex、sleep、I/O 操作 |
| 延迟影响 | 直接增加调度延迟 | 通过优先级管理隔离延迟 |
2.5 线程化中断的优先级与调度策略
每个线程化中断对应一个内核线程,命名格式为 irq/中断号-设备名。驱动开发者可以通过以下方式调整其实时属性:
// 获取线程化中断的内核线程指针
struct task_struct *tsk = irq_to_thread(irq_num);
if (tsk) {
// 设置实时调度策略和优先级
struct sched_param param = { .sched_priority = 80 };
sched_setscheduler(tsk, SCHED_FIFO, ¶m);
}
优先级配置警告: 线程化中断的优先级不应高于系统中关键实时任务的优先级,否则可能导致实时任务被中断线程阻塞,反而恶化实时性能。通常建议将中断线程优先级设置在实时任务优先级之下。
2.6 中断线程化的内核实现机制
内核通过 irq_thread 内核线程实现中断处理的下半部迁移,其核心逻辑如下:
// 内核线程主循环(简化)
static int irq_thread(void *data) {
struct irqaction *action = data;
struct irq_desc *desc = irq_to_desc(action->irq);
int wake;
while (!kthread_should_stop()) {
// 等待硬中断处理函数唤醒
wait_event_interruptible(desc->wait_for_threads,
(wake = atomic_read(&desc->threads_active)));
if (wake) {
// 执行线程化处理函数
action->thread_fn(action->irq, action->dev_id);
// 清除唤醒标志
atomic_dec(&desc->threads_active);
}
}
return 0;
}
2.7 驱动兼容性关键点
当中断被线程化后,驱动开发者必须注意以下上下文变化:
- 锁机制替换:线程化处理函数中不能再使用
spin_lock_irqsave(),应替换为mutex_lock()或spin_lock()(取决于是否需要在硬中断上下文中访问共享数据)。 - 中断屏蔽行为:线程化处理函数中调用
local_irq_disable()仅屏蔽当前线程所在 CPU 的中断,不会影响其他 CPU。 - 硬件访问时序:从硬中断触发到线程化处理函数执行之间存在调度延迟,硬件状态可能发生变化,驱动需要重新读取硬件寄存器确认状态。
- 中断共享:多个设备共享同一 IRQ 时,线程化中断的唤醒逻辑需要正确处理
IRQ_WAKE_THREAD返回值,避免误唤醒。
迁移检查清单: 将传统驱动迁移到线程化中断时,建议逐项检查:① 所有 spinlock 是否可替换为 mutex;② 硬中断处理函数是否足够短(< 20 µs);③ 线程化处理函数中是否有阻塞操作;④ 中断共享逻辑是否正确处理返回值。