4. Linux中断处理架构

在实时系统中,中断响应延迟是衡量系统实时性能的核心指标。Linux 内核为了在中断响应速度与系统整体吞吐量之间取得平衡,将中断处理过程拆分为两个阶段:上半部(Top Half) 与 下半部(Bottom Half)。这种架构设计是理解中断线程化改造的基础。

4.1 上半部(Top Half)与下半部(Bottom Half)的设计哲学

中断处理程序执行时,CPU 会屏蔽同级或更低级的中断,导致系统无法响应其他中断事件。如果中断处理程序执行时间过长,将严重影响系统的实时性和吞吐量。因此,内核将中断处理拆分为两部分:

  • 上半部(Top Half):在中断上下文中立即执行,要求快速、简短、不可阻塞。通常只完成最紧急的硬件操作,如清除中断标志、拷贝硬件寄存器数据等。
  • 下半部(Bottom Half):推迟到稍后时间执行,运行在软中断上下文或进程上下文中,可以处理更复杂的任务,如协议栈处理、数据拷贝等。
关键原则:上半部执行时间应控制在微秒级,仅完成必须立即响应的硬件操作;其余所有可推迟的工作全部交由下半部处理。
实时性启示:上半部执行期间中断被屏蔽,这是导致中断延迟抖动的核心来源。中断线程化的目标就是将上半部的屏蔽时间压缩到极致,甚至将整个上半部线程化。

4.2 下半部机制演进:从软中断到工作队列

Linux 内核提供了多种下半部实现机制,它们在执行上下文、可阻塞性、实时性方面存在显著差异。下表总结了三种主要机制:

特性 软中断(SoftIRQ) Tasklet 工作队列(Workqueue)
执行上下文 软中断上下文(原子) 软中断上下文(原子) 进程上下文(可睡眠)
可阻塞/睡眠 否 否 是
并发性 同一类型可在多核并行 同一类型串行执行 由 worker pool 管理,可并行
实时性影响 高优先级,但不可抢占 中等,不可抢占 低优先级,可被高优先级任务抢占
典型用途 网络 RX/TX、定时器 驱动下半部、简单推迟任务 复杂、可阻塞的推迟任务

4.3 软中断(SoftIRQ)机制详解

软中断是内核中优先级最高的下半部机制,直接由 __do_softirq() 函数在中断返回路径上执行。软中断在编译时静态分配,系统定义了有限的软中断类型(如 NET_RX_SOFTIRQ、TASKLET_SOFTIRQ 等)。

// 软中断类型定义(kernel/softirq.c)
enum {
    HI_SOFTIRQ = 0,       // 高优先级 tasklet
    TIMER_SOFTIRQ,        // 定时器
    NET_TX_SOFTIRQ,       // 网络发送
    NET_RX_SOFTIRQ,       // 网络接收
    BLOCK_SOFTIRQ,        // 块设备
    IRQ_POLL_SOFTIRQ,     // 中断轮询
    TASKLET_SOFTIRQ,      // 普通 tasklet
    SCHED_SOFTIRQ,        // 调度器
    HRTIMER_SOFTIRQ,      // 高精度定时器
    RCU_SOFTIRQ,          // RCU 处理
    NR_SOFTIRQS
};

软中断的执行具有以下特点:

  • 在 irq_exit() 中被调用,运行在中断上下文中。
  • 同一类型的软中断可以在多个 CPU 上同时执行,但必须保证数据结构的并发安全。
  • 软中断执行期间,可以被更高优先级的中断打断,但不能被其他软中断抢占。
实时性风险:软中断在中断上下文中执行,不可被实时任务抢占。如果网络软中断处理时间过长(如处理大量数据包),将导致实时任务错过截止时间。这是 PREEMPT_RT 补丁需要将软中断线程化的核心原因之一。

4.4 Tasklet 机制:基于软中断的简化封装

Tasklet 是建立在软中断之上的更易用的下半部接口。它使用 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两个软中断类型,通过链表管理多个 tasklet 实例。

// Tasklet 使用示例
#include <linux/interrupt.h>

// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data) {
    // 处理推迟的工作,不可睡眠
    printk("Tasklet executed on CPU %d\n", smp_processor_id());
}

// 声明并初始化 tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);

// 在中断上半部中调度 tasklet
irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
    // 快速硬件操作
    // ...
    // 调度下半部
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}

Tasklet 与软中断的关键区别:

  • 串行性:同一 tasklet 实例在任何时刻只能在一个 CPU 上执行,避免了并发问题。
  • 动态性:tasklet 可以在模块加载时动态创建和注册。
  • 优先级:HI_SOFTIRQ 类型的 tasklet 优先级高于普通 tasklet。

4.5 工作队列(Workqueue)机制:进程上下文的下半部

工作队列将下半部任务推迟到内核线程中执行,运行在进程上下文中,因此可以睡眠、持有信号量、执行可能阻塞的 I/O 操作。工作队列是唯一可以在下半部中安全使用阻塞操作的内核机制。

// 工作队列使用示例
#include <linux/workqueue.h>

// 定义工作处理函数
void my_work_handler(struct work_struct *work) {
    // 可以睡眠,可以持有 mutex
    msleep(10);
    printk("Work executed in process context\n");
}

// 声明工作项
DECLARE_WORK(my_work, my_work_handler);

// 在中断上半部中调度工作
irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
    // 快速硬件操作
    // ...
    // 调度工作队列
    schedule_work(&my_work);
    return IRQ_HANDLED;
}

工作队列的演进:

  • 传统工作队列:每个 CPU 一个 events/n 内核线程,所有工作项共享该线程。
  • CMWQ(Concurrency Managed Workqueue):Linux 3.x 引入,使用 worker pool 动态管理线程数量,避免线程过多导致的上下文切换开销。

4.6 中断处理全流程架构图

下图展示了从硬件中断触发到上半部、下半部处理的完整流程:

Linux 中断处理架构:上半部与下半部流程 硬件中断触发 上半部(Top Half) 中断上下文 · 原子操作 · 快速执行 软中断(SoftIRQ) 不可睡眠 · 高优先级 Tasklet 不可睡眠 · 串行执行 工作队列(Workqueue) 可睡眠 · 进程上下文 执行细节 irq_exit() → __do_softirq() 多核可并行执行 执行细节 tasklet_schedule() 触发 同一实例串行执行 执行细节 schedule_work() 触发 worker pool 管理线程 中断返回 · 恢复被中断任务 PREEMPT_RT 改造:将软中断、Tasklet 线程化,使其运行在可抢占的进程上下文中

4.7 下半部选择决策指南

在实际驱动开发中,选择哪种下半部机制取决于任务的特性和实时性要求:

  1. 任务是否可以睡眠?
    • 是 → 使用工作队列(Workqueue)
    • 否 → 考虑软中断或 Tasklet
  2. 是否需要多核并行执行?
    • 是 → 使用软中断(需自行处理并发)
    • 否 → 使用 Tasklet(自动串行化)
  3. 实时性要求是否苛刻?
    • 是 → 避免使用软中断,优先使用工作队列(可被抢占)
    • 否 → 根据性能需求选择
工程实践建议:在实时系统中,应尽量避免在软中断和 Tasklet 中执行耗时操作。对于网络驱动等高性能场景,可考虑使用 NAPI(New API)结合软中断,但需配合中断节流(Interrupt Coalescing)技术控制处理时间。

4.8 中断线程化的前置理解

理解上半部与下半部的划分机制,是掌握中断线程化的前提。中断线程化的核心思想是:

  • 将原本在中断上下文中执行的上半部代码,移入内核线程中执行。
  • 将软中断和 Tasklet 也线程化,使其运行在可被实时任务抢占的进程上下文中。
  • 通过实时调度策略(如 SCHED_FIFO)控制中断处理线程的优先级,确保关键中断的实时响应。