4. 中断线程化原理:将中断处理程序下半部移至内核线程,使中断可被调度与抢占

中断线程化(Interrupt Threading)是实时操作系统与嵌入式 Linux 中解决“中断不可抢占”问题的核心手段。其基本思想是将中断处理程序分为两部分:上半部(Hard IRQ Context) 执行必须关中断的极简操作(如清中断标志、拷贝硬件寄存器),下半部(Threaded IRQ) 则作为一个可被调度、可被抢占的内核线程运行,允许其他高优先级任务或实时线程在中断处理期间获得 CPU 控制权。

核心收益:中断线程化后,中断处理不再“锁死”整个系统,实时任务可以在中断下半部执行期间抢占 CPU,从而大幅降低最大关中断时间与任务响应延迟抖动。

4.1 中断处理的分段模型

传统中断处理模型将全部逻辑放在关中断上下文中执行,导致系统对中断的响应延迟不可控。中断线程化通过引入“硬中断上下文 + 内核线程上下文”的两段模型,将可延迟执行的工作剥离出来。

阶段 执行上下文 可抢占性 典型操作
上半部(Top Half) 硬中断上下文(IRQ off) 不可抢占,关中断 读取 FIFO、清中断挂起、唤醒线程
下半部(Bottom Half) 内核线程上下文(可睡眠) 可被调度与抢占 协议解析、数据拷贝、通知用户态
设计原则:上半部应尽量短小,只做“必须关中断才能做的事”;其余所有可延迟处理的工作全部推入线程化下半部。这直接决定了系统最大关中断时间。

4.2 中断线程化的调度与抢占机制

当中断发生时,硬件自动关闭本地 CPU 中断,执行上半部。上半部结束后,内核将对应的线程化中断处理函数挂入内核线程的调度队列。该线程拥有独立的 task_struct,可以被调度器按优先级调度,也可以被更高优先级的实时任务或中断线程抢占。

中断线程化调度与抢占流程 硬件中断触发 上半部:关中断,极简操作 唤醒线程化下半部 内核调度器:优先级排队 可被高优任务/中断线程抢占 关中断时间 = 上半部耗时 下半部可被任意抢占 线程化 IRQ 优先级 可独立设置,支持优先级继承

4.3 线程化中断的优先级与抢占规则

每个线程化中断都对应一个内核线程,其优先级可通过 request_threaded_irq() 或 irq_set_affinity_hint() 配合调度策略进行设置。抢占规则遵循标准 Linux 实时调度策略(SCHED_FIFO / SCHED_RR):

  • 优先级比较:线程化中断线程的优先级通常设置为高于普通任务但低于硬实时任务,确保中断处理不被无限阻塞。
  • 可抢占条件:当更高优先级的任务或中断线程就绪时,当前正在执行的线程化下半部会被立即抢占,保存上下文后让出 CPU。
  • 优先级反转防护:线程化中断线程在持有自旋锁或互斥锁时,若被低优先级任务阻塞,可通过优先级继承协议(Priority Inheritance)临时提升阻塞任务的优先级。
工程陷阱:线程化中断虽然可被抢占,但如果上半部执行时间过长(例如超过 10μs),仍然会破坏实时性。务必使用示波器或 ftrace 测量实际关中断时间,确保上半部只做“原子级”操作。

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,避免优先级反转。
  • 性能权衡:线程化增加了上下文切换开销,但在实时性要求高的场景下,确定性比吞吐量更重要。
PREEMPT_RT 中断线程化架构 硬件中断 上半部(极简) 线程化下半部 RT 调度器 实时任务(抢占) 所有中断默认线程化 关中断时间 < 5μs 实时任务可抢占中断线程 调度延迟确定性 < 100μs

4.7 中断线程化移植的关键步骤

  1. 识别可线程化的中断:分析现有驱动,将非原子操作(如数据拷贝、协议解析、I2C/SPI 通信)从上半部剥离。
  2. 拆分处理函数:上半部只保留寄存器读写、中断标志清除、唤醒线程;下半部实现全部业务逻辑。
  3. 替换注册接口:将 request_irq() 改为 request_threaded_irq(),并确保上半部返回 IRQ_WAKE_THREAD。
  4. 锁机制适配:线程化下半部中可使用 mutex 或 spin_lock_irqsave(取决于是否与上半部共享数据),避免在关中断上下文中使用睡眠锁。
  5. 优先级调优:通过 sched_setscheduler() 或 irq_set_affinity_hint() 设置线程化中断的实时优先级,确保关键中断不被饿死。
  6. 验证关中断时间:使用 ftrace 或 trace_preemptirqsoff 测量最大关中断时间,确认优化效果。
移植检查清单:
  • 上半部是否只包含必须关中断的操作?
  • 下半部是否使用了可睡眠的锁或 API?
  • 线程化中断的优先级是否高于所有可能阻塞它的普通任务?
  • 是否已测量并记录移植前后的最大关中断时间?

中断线程化是嵌入式实时系统从“裸机中断思维”转向“可抢占内核思维”的关键一步。掌握其原理与移植方法,是后续优化抢占临界区、实现微秒级响应的重要基础。