Linux内核实时性补丁驱动中断线程化移植实战工程 - 第4节

4. 中断线程化原理

在理解中断线程化之前,必须厘清 Linux 内核中两种最基本的执行上下文:中断上下文(Interrupt Context)与进程上下文(Process Context)。二者的差异直接决定了中断处理程序的设计约束,也是 PREEMPT_RT 补丁引入中断线程化的根本原因。

4.1.1 上下文定义与核心差异

对比维度 中断上下文 进程上下文
触发方式 硬件中断信号(IRQ) 系统调用、异常、内核线程
可睡眠 ❌ 不可睡眠(不能调用 schedule()) ✅ 可睡眠(可被调度器换出)
可被抢占 默认不可抢占(关中断或关抢占) 可被更高优先级任务抢占
持有锁类型 只能使用 spin_lock(自旋锁) 可使用 mutex、semaphore 等睡眠锁
current 指针 指向被中断的进程(不可信) 指向当前运行的进程
执行时间要求 极短(μs 级),否则丢中断 可较长(ms 级甚至更久)
关键结论:中断上下文是内核中最高优先级的执行实体,但受限于“不可睡眠”和“不可长时间占用 CPU”的硬约束。这导致复杂的中断处理逻辑(如网络协议栈、块设备 I/O)无法直接在中断上下文中完成,必须拆分为“上半部/下半部”机制。

4.1.2 中断上下文的内核栈与状态

每个 CPU 在中断发生时,会从进程栈切换到专用的中断栈(Interrupt Stack)。中断栈大小通常为 4KB~16KB,远小于进程栈(通常 8KB~16KB)。这意味着中断处理程序必须严格控制栈使用深度,避免递归或大局部变量。

调试技巧:在 /proc/interrupts 中可查看每个 CPU 的中断计数;在 /proc/softirqs 中可查看软中断统计。若某个中断处理函数执行时间过长,可通过 ftrace 的 irqsoff tracer 捕获关中断时长。
中断上下文 vs 进程上下文切换示意 进程上下文 用户态 / 内核态系统调用 可睡眠、可抢占 持有 mutex / semaphore current 指向当前进程 进程栈(8KB~16KB) 中断上下文 硬件 IRQ 触发 不可睡眠、不可长时间占用 只能使用 spin_lock current 指向被中断进程 中断栈(4KB~16KB) 硬件中断 触发切换 中断上下文不能调用 schedule(),否则导致内核崩溃(内核 BUG)

4.2 中断线程化的动机

传统 Linux 内核的中断处理模型存在一个根本矛盾:实时性要求与中断不可抢占性之间的冲突。高优先级实时任务可能因为一个低优先级设备的中断处理而被阻塞,导致响应延迟不可控。

4.2.1 传统中断模型的实时性缺陷

  • 中断优先级高于任何进程:即使实时任务优先级为 99,中断处理程序仍会抢占它。低优先级外设的中断可能延迟高优先级任务的执行。
  • 关中断导致抢占失效:中断处理期间(尤其是 spin_lock 持有期间),本地 CPU 关中断或关抢占,实时任务无法被调度。
  • 中断嵌套不可控:高优先级中断可以嵌套低优先级中断,导致执行顺序与任务优先级无关,破坏实时调度策略。
  • 下半部(softirq/tasklet)仍在软中断上下文:软中断在 do_softirq() 中执行,仍然不可睡眠,且优先级高于普通进程。
典型问题场景:一个网卡中断处理程序需要 200μs 完成数据包接收,但此时一个优先级为 99 的实时控制任务需要 50μs 内响应。由于中断抢占,控制任务的 deadline 被错过,导致系统失控。

4.2.2 中断线程化的核心思想

PREEMPT_RT 补丁引入的中断线程化(Interrupt Threading)将大部分中断处理程序(IRQ handler)从原子中断上下文迁移到内核线程(kthread)中执行。每个 IRQ 可以绑定一个实时优先级的内核线程,由调度器统一管理。

特性 传统中断模型 中断线程化模型
执行上下文 中断上下文(原子、不可睡眠) 进程上下文(可睡眠、可抢占)
优先级管理 硬件中断优先级(固定) 内核线程优先级(可配置,遵循 SCHED_FIFO/RR)
可睡眠 ❌ ✅(可使用 mutex、wait_queue)
锁兼容性 仅 spin_lock spin_lock + mutex
实时性控制 不可控(中断优先级高于一切) 可控(调度器按优先级调度)
中断延迟 低(直接硬件响应) 略高(线程调度开销)
本质变化:中断线程化将“中断处理”从硬件驱动的紧急响应,转变为“可被调度器管理的实时任务”。实时内核不再被硬件中断优先级绑架,而是通过调度策略保证关键任务的截止时间。
中断线程化前后对比 传统中断模型 硬件 IRQ 中断处理程序 softirq/tasklet 进程上下文 中断不可睡眠,优先级高于所有进程 中断线程化模型 硬件 IRQ 最小顶半部 (唤醒线程) 中断线程 (irq/xxx) 进程上下文 中断线程可睡眠,由调度器管理优先级

4.3 软中断与 tasklet 的线程化改造

软中断(softirq)和 tasklet 是 Linux 内核传统的下半部机制。在 PREEMPT_RT 中,它们被进一步线程化,以消除软中断上下文中的不可抢占性。

4.3.1 软中断的传统实现

软中断在 do_softirq() 中执行,通常由 irq_exit() 在中断返回时调用。软中断运行在软中断上下文(softirq context)中,仍然不可睡眠,且优先级高于普通进程。

// 传统软中断执行路径(非 RT 内核)
irq_handler() → irq_exit() → do_softirq() → 执行软中断向量
// 软中断向量包括:NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, TASKLET_SOFTIRQ 等

4.3.2 PREEMPT_RT 的软中断线程化

PREEMPT_RT 补丁将软中断的执行从 do_softirq() 迁移到每个 CPU 的软中断内核线程(ksoftirqd)中。ksoftirqd 是一个 SCHED_FIFO 优先级为 1 的内核线程,可以被更高优先级的实时任务抢占。

组件 传统内核 PREEMPT_RT 内核
软中断执行位置 do_softirq()(软中断上下文) ksoftirqd 内核线程(进程上下文)
可睡眠 ❌ ✅
优先级 高于所有进程 SCHED_FIFO 优先级 1(可被更高优先级任务抢占)
tasklet 实现 基于 TASKLET_SOFTIRQ 软中断 基于 tasklet_thread 内核线程
调度延迟 低(直接执行) 略高(线程切换开销)

4.3.3 tasklet 的线程化改造细节

tasklet 在传统内核中本质是软中断的一种特殊形式(TASKLET_SOFTIRQ)。PREEMPT_RT 将其改造为基于tasklet_thread 的内核线程执行模型:

  1. tasklet_schedule() 不再触发软中断,而是唤醒绑定的 tasklet 内核线程。
  2. tasklet 内核线程 以 SCHED_FIFO 优先级运行,可被实时任务抢占。
  3. tasklet 内部可以使用 mutex,因为现在运行在进程上下文中。
  4. 兼容性层:未修改的 tasklet 代码无需改动,底层机制自动切换。
移植注意事项:在 PREEMPT_RT 内核中,tasklet 的执行语义发生了变化。虽然 API 接口不变,但 tasklet 的执行延迟可能增加(因为需要线程调度)。对于时间极其敏感的下半部,建议直接使用实时内核线程(kthread)替代 tasklet。
软中断与 tasklet 线程化改造架构 硬件中断处理程序 传统:do_softirq() 软中断向量 RT:ksoftirqd 线程 线程化软中断 RT:tasklet_thread 线程化 tasklet 虚线路径表示 PREEMPT_RT 的线程化改造:软中断 → ksoftirqd,tasklet → tasklet_thread 线程化后,下半部运行在进程上下文,可被实时任务抢占

4.3.4 线程化改造的收益与代价

评估维度 收益 代价
实时性 高优先级实时任务可抢占中断线程,保证 deadline 中断响应延迟增加(线程调度 + 上下文切换)
锁兼容性 中断线程可使用 mutex,简化驱动开发 spin_lock 在 RT 内核中变为可睡眠锁(rt_mutex)
代码复用 现有驱动代码无需修改 API 需验证 tasklet 执行顺序依赖(线程化后可能乱序)
CPU 开销 减少关中断时间,提升系统整体响应 线程切换增加 CPU 负载(约 5%~15%)
移植建议:对于实时性要求极高的中断(如 1kHz 以上的控制中断),建议保留最小顶半部(仅做硬件 ACK 和唤醒线程),将全部业务逻辑放入中断线程。对于非实时设备(如键盘、鼠标),可直接使用默认的线程化配置。

4.4 本章小结

  • 中断上下文不可睡眠、不可长时间占用,是实时性的主要瓶颈。
  • 中断线程化将中断处理迁移到进程上下文,由调度器统一管理优先级。
  • 软中断被线程化为 ksoftirqd 内核线程(SCHED_FIFO 优先级 1)。
  • tasklet 被线程化为 tasklet_thread,可在进程上下文中使用 mutex。
  • 线程化改造以轻微增加中断延迟为代价,换取确定性的实时调度能力。