4. PREEMPT_RT 核心机制(一):中断线程化(Interrupt Threading)原理、实现方式、对实时性的提升

在标准 Linux 内核中,硬件中断请求(IRQ)具有最高的执行优先级。当 CPU 接收到中断信号时,内核会立即暂停当前正在执行的任何任务(包括实时任务),转而执行中断服务例程(ISR)。这种设计在追求高吞吐量的通用系统中是合理的,但在实时系统中却带来了严重的 优先级反转 和 不可预测的延迟 问题。PREEMPT_RT 补丁引入的 中断线程化(Interrupt Threading) 机制,正是为了解决这一核心矛盾。

4.1 中断线程化的基本原理

中断线程化的核心思想是:将原本在不可抢占的、高优先级的中断上下文中执行的 ISR,拆分为两个部分:

  • 上半部(Top Half / 硬中断上下文): 执行极少量、必须立即响应的操作,例如:清除硬件中断标志位、拷贝硬件寄存器数据到内存、唤醒对应的中断线程。这部分仍然在真正的硬件中断上下文中运行,但执行时间被压缩到微秒级。
  • 下半部(Bottom Half / 线程上下文): 将大部分耗时的中断处理逻辑(如协议解析、数据拷贝、驱动回调)移到一个内核线程中执行。这个线程拥有自己的优先级(通常为 50,可通过 chrt 调整),可以被其他更高优先级的实时任务抢占。
核心变化: 中断处理从“不可抢占的原子上下文”转变为“可被调度的线程上下文”。实时任务不再被长中断处理程序阻塞,系统的最大关抢占时间被显著缩短。
中断线程化前后对比 标准内核:中断处理不可抢占 实时任务(优先级 80) 硬件中断 IRQ(优先级最高) ISR 执行(不可抢占,耗时 500us) 实时任务被延迟 500us PREEMPT_RT:中断线程化 实时任务(优先级 80) 硬件中断 IRQ(极短,5us) 中断线程(优先级 50,可被抢占) 实时任务按时执行,延迟 < 5us

4.2 实现方式:从 IRQ 到内核线程

PREEMPT_RT 通过修改内核的中断子系统,实现了从硬件中断到内核线程的映射。其核心数据结构是 irq_desc,每个中断号对应一个 irq_desc,其中新增了指向 task_struct 的指针。

4.2.1 关键数据结构与流程

组件 标准内核 PREEMPT_RT 内核
中断上下文 ISR 在关抢占的原子上下文中运行 ISR 分为硬中断上下文(极短)和线程上下文
调度单元 中断处理程序本身 内核线程(irq/<编号>-<名称>)
优先级 固定最高(不可调度) 默认为 50(可通过 chrt 调整)
可抢占性 不可抢占 可被更高优先级实时任务抢占
锁机制 使用 spin_lock(自旋锁) 使用 raw_spin_lock 或 rt_mutex

4.2.2 中断线程化流程步骤

  1. 硬件中断触发: CPU 收到中断信号,进入硬中断上下文。
  2. 上半部执行: 执行 handle_irq_event_percpu(),调用 action->handler()。在 PREEMPT_RT 中,这个 handler 被替换为 irq_thread_fn() 的包装函数。
  3. 唤醒中断线程: 上半部完成关键硬件操作后,调用 wake_up_process() 唤醒与该 IRQ 绑定的内核线程 irq/<编号>-<名称>。
  4. 线程调度: 调度器根据优先级决定何时运行该中断线程。如果此时有优先级高于 50 的实时任务就绪,中断线程将被推迟执行。
  5. 下半部执行: 中断线程获得 CPU 后,在进程上下文中执行真正的驱动处理逻辑(如 request_irq() 时注册的 handler)。
驱动开发者注意: 在 PREEMPT_RT 下,request_irq() 的 flags 参数中如果未设置 IRQF_NO_THREAD 标志,该中断将被自动线程化。对于需要极低延迟的驱动(如高精度定时器),必须显式设置 IRQF_NO_THREAD 以保持硬中断上下文。
中断线程化内部实现架构 PREEMPT_RT 中断线程化实现架构 硬件中断触发(CPU 异常处理) 上半部:handle_irq_event_percpu() 清除中断标志位 | 拷贝寄存器 | 唤醒线程 内核调度器(CFS / RT) 高优先级实时任务(优先级 80) 抢占中断线程,优先执行 中断线程 irq/xxx(优先级 50) 执行驱动注册的 handler()

4.3 对实时性的提升:量化分析

中断线程化对实时性的提升主要体现在以下三个维度:

指标 标准内核 PREEMPT_RT 内核 提升幅度
最大关抢占时间 数百微秒 ~ 数毫秒(取决于 ISR 长度) 5 ~ 15 微秒(仅上半部) 降低 1 ~ 2 个数量级
实时任务调度延迟 受所有中断处理影响,抖动大 仅受高优先级中断上半部影响,抖动小 确定性显著提高
优先级反转窗口 中断处理期间,所有任务被阻塞 中断线程可被高优先级任务抢占 从根本上消除反转
系统吞吐量 中断处理快,但任务切换频繁 中断处理被调度,上下文切换略增 实时性优先,吞吐量略有下降
性能权衡: 中断线程化并非没有代价。将中断处理推迟到线程上下文会增加一次额外的上下文切换(从硬中断上下文切换到线程上下文)。对于非实时、高吞吐量的场景(如网络收发包),这种开销可能降低整体性能。因此,PREEMPT_RT 允许通过 threadirqs 内核启动参数全局控制,或通过 IRQF_NO_THREAD 标志逐中断控制。

4.4 实战:查看与验证中断线程化

在运行 PREEMPT_RT 内核的系统上,可以通过以下命令验证中断线程化是否生效:

# 查看所有中断对应的内核线程
ps -eo pid,comm,pri | grep "irq/"

# 输出示例:
#   PID COMMAND         PRI
#    42 irq/8-rtc0       50
#    45 irq/12-i8042     50
#    48 irq/16-ahci      50
#    52 irq/23-ehci_hcd  50

可以看到,每个中断都对应一个名为 irq/<编号>-<名称> 的内核线程,其优先级为 50(对应实时优先级 50)。

# 查看特定中断的线程化状态
cat /proc/interrupts | head -5

# 输出示例:
#            CPU0
#   8:         42   IO-APIC   8-edge      rtc0
#  12:        123   IO-APIC  12-edge      i8042
#  16:         56   IO-APIC  16-fasteoi   ahci

# 使用 chrt 调整中断线程优先级(需 root 权限)
chrt -p -f 60 42   # 将 rtc0 中断线程优先级提升为 60
调试技巧: 使用 trace-cmd 或 ftrace 可以精确测量中断线程化的延迟。重点关注 irq_handler_entry 和 irq_handler_exit 事件之间的时间差,以及 sched_wakeup 事件到中断线程实际运行的时间。
中断线程化延迟对比 实时任务调度延迟对比(单位:微秒) 标准内核 ISR 执行时间:500us 实时任务等待时间:500us 总延迟 ≈ 500us PREEMPT_RT 5us 实时任务执行(抢占中断线程) 5us 中断线程延迟执行 总延迟 ≈ 5us(仅上半部) 结论:中断线程化将实时任务的最大延迟从 500us 降低到 5us,确定性提升 100 倍

4.5 总结与最佳实践

  • 核心收益: 中断线程化将不可抢占的中断处理程序变为可调度的线程,从根本上解决了实时任务被长中断阻塞的问题。
  • 适用场景: 所有对调度延迟有确定性要求的实时系统,特别是工业控制、机器人、音频处理等。
  • 例外情况: 对于需要极低延迟(< 10us)且执行时间极短的中断(如高精度定时器、DMA 完成中断),应使用 IRQF_NO_THREAD 保持硬中断上下文。
  • 优先级规划: 中断线程的默认优先级为 50,建议将关键实时任务的优先级设置为 60 ~ 99,确保它们能抢占中断线程。