4. 中断线程化原理:中断上半部与下半部
中断线程化是 PREEMPT_RT 补丁对 Linux 中断处理机制最核心的改造之一。其目标是将原本在硬中断上下文(不可抢占、不可睡眠)中执行的中断处理程序,迁移到可睡眠、可被抢占的内核线程上下文中执行,从而大幅降低系统的最坏情况延迟(Worst-Case Latency),并消除优先级反转等实时性问题。
4.1 传统中断处理:上半部与下半部
在标准 Linux 内核中,中断处理被明确划分为两个阶段:
- 上半部(Top Half):在硬中断上下文中立即执行,必须快速、简短、不可睡眠。通常只完成最紧急的硬件操作(如清除中断标志、拷贝 FIFO 数据),然后触发下半部。
- 下半部(Bottom Half):在稍后的时间点执行,允许被其他中断打断,但仍然运行在软中断(SoftIRQ)或 tasklet 上下文中,不可睡眠。常见的下半部机制包括 SoftIRQ、Tasklet 和 Workqueue。
关键问题: 传统下半部(SoftIRQ/Tasklet)虽然比上半部灵活,但仍然运行在中断上下文中,无法被高优先级实时任务抢占,导致实时任务必须等待所有挂起的软中断处理完毕才能获得 CPU,这引入了不可预测的延迟抖动(Jitter)。
下表总结了传统中断处理各阶段的特性对比:
| 特性 | 上半部 (Top Half) | 下半部 (Bottom Half) | 说明 |
|---|---|---|---|
| 执行上下文 | 硬中断上下文 | 软中断上下文 | 均不可睡眠 |
| 可抢占性 | 不可抢占 | 不可被进程抢占 | 可被更高优先级中断打断 |
| 允许操作 | 极简硬件操作 | 数据处理、唤醒任务 | 禁止获取信号量、禁止睡眠 |
| 延迟影响 | 直接增加中断延迟 | 阻塞实时任务调度 | 主要抖动来源 |
实时性痛点: 在标准内核中,一个高优先级实时任务可能因为一个正在执行的 SoftIRQ 而延迟数百微秒甚至毫秒,这在工业控制或音频处理场景中是不可接受的。
4.2 中断线程化机制
PREEMPT_RT 补丁引入了中断线程化(Interrupt Threading)机制,将中断处理程序的大部分工作(即原来的下半部逻辑)移到一个内核线程中执行。这个线程具有以下特点:
- 可睡眠:线程化后的中断处理程序可以调用
wait_event()、mutex_lock()等可能导致睡眠的函数。 - 可被抢占:该线程拥有自己的优先级(通常继承自中断号或用户配置),可以被更高优先级的实时任务或中断线程抢占。
- 独立调度实体:作为 SCHED_FIFO 或 SCHED_RR 实时线程参与内核调度。
中断线程化的核心 API 是 request_threaded_irq(),其函数原型如下:
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);
参数说明:
irq:中断号。handler:上半部函数(硬中断上下文)。如果为NULL,则内核会自动调用irq_default_primary_handler(),该函数立即唤醒线程。thread_fn:线程化处理函数(线程上下文)。这是实际执行大部分工作的函数。irqflags:中断标志,如IRQF_TRIGGER_RISING、IRQF_SHARED等。devname:设备名称。dev_id:设备 ID,用于共享中断。
重要规则: 如果
handler 返回 IRQ_WAKE_THREAD,则内核会唤醒对应的中断线程来执行 thread_fn。如果 handler 返回 IRQ_HANDLED,则不会唤醒线程。这允许驱动在极少数情况下(如快速确认硬件状态)选择不线程化处理。
下图展示了中断线程化的完整处理流程:
4.3 强制线程化与自愿线程化策略
PREEMPT_RT 提供了两种中断线程化策略,以适应不同的硬件和驱动场景:
4.3.1 强制线程化 (Forced Threading)
当内核配置了 CONFIG_IRQ_FORCED_THREADING 选项(在 PREEMPT_RT 内核中默认启用),所有中断处理程序必须被线程化。即使驱动调用的是传统的 request_irq(),内核也会自动将其转换为线程化形式。
- 触发条件:内核启动参数
threadirqs或在 PREEMPT_RT 内核中默认生效。 - 行为:内核为每个中断创建一个内核线程,名为
irq/<irq_number>-<device_name>。 - 优点:统一所有中断处理行为,彻底消除软中断导致的不可抢占问题。
- 缺点:对于极低延迟要求的硬件(如某些高速网络设备),线程切换开销可能影响吞吐量。
4.3.2 自愿线程化 (Voluntary Threading)
驱动开发者通过显式调用 request_threaded_irq() 并合理设计上半部与线程函数,自主决定哪些中断需要线程化。这是更精细的控制方式。
- 触发条件:驱动代码显式使用
request_threaded_irq()。 - 行为:只有调用该 API 的中断会被线程化,其他使用
request_irq()的中断保持传统方式。 - 优点:驱动开发者可以针对硬件特性优化,例如将时间关键型操作留在上半部。
- 缺点:需要驱动开发者对实时性有深入理解,否则可能引入新的问题。
实践建议: 在 PREEMPT_RT 系统中,推荐使用强制线程化作为基础策略,然后针对性能关键型设备(如 GPU、高速网卡)通过驱动优化实现自愿线程化的混合模式。
下表对比了两种策略的关键差异:
| 对比维度 | 强制线程化 | 自愿线程化 |
|---|---|---|
| 控制粒度 | 系统全局 | 每个中断独立 |
| 配置方式 | 内核配置 + 启动参数 | 驱动代码 API 调用 |
| 上半部执行 | 仅确认中断、唤醒线程 | 可包含自定义快速处理 |
| 适用场景 | 通用实时系统、工业控制 | 高性能网络、音视频处理 |
| 延迟确定性 | 高(统一调度) | 中(依赖驱动实现) |
4.4 中断线程的优先级与调度
每个中断线程都是一个独立的实时内核线程,其优先级管理遵循以下规则:
- 默认优先级:中断线程的优先级由中断号决定,通常
prio = 50 - irq_number(数值越小优先级越高)。这确保了低中断号(高优先级中断)的线程获得更高的调度优先级。 - 用户自定义:可以通过
chrt命令或sched_setscheduler()系统调用动态调整中断线程的优先级和调度策略。 - 优先级继承:当中断线程持有互斥锁(mutex)并被高优先级任务阻塞时,PREEMPT_RT 的优先级继承协议(Priority Inheritance Protocol)会临时提升中断线程的优先级,防止优先级反转。
调试技巧: 使用
ps -eo pid,pri,rtprio,cmd | grep irq/ 可以查看系统中所有中断线程的实时优先级。例如:irq/50-i2c 的默认优先级为 0(最高),而 irq/130-eth0 的优先级为 -80(较低)。
4.5 中断线程化的实际影响
中断线程化对系统行为产生了深远影响,主要体现在以下几个方面:
- 上下文切换开销:每次中断处理至少需要两次上下文切换(从进程上下文到内核线程,再从内核线程返回),增加了中断延迟的绝对值。
- 缓存污染减少:由于中断线程可以被抢占,高优先级实时任务可以更快获得 CPU,减少了因中断处理导致的缓存污染。
- 锁机制变化:中断处理程序不再运行在原子上下文中,因此可以使用
mutex_lock()替代spin_lock_irqsave(),简化了驱动开发。 - 中断嵌套:线程化的中断处理程序可以被更高优先级的中断线程抢占,实现了中断的嵌套处理,进一步提高了实时性。
注意事项: 中断线程化并非万能。对于某些需要极低延迟(微秒级)的硬件,如 FPGA 或高速数据采集卡,线程切换的开销可能仍然过大。此时需要结合硬件中断控制器(如 GIC)的优先级管理功能,将关键中断配置为快速中断(FIQ)或使用 NAPI 轮询机制。
下图展示了中断线程化前后系统调度行为的对比: