3、Linux中断子系统框架:IRQ Domain、中断描述符、中断控制器抽象层

Linux 中断子系统是内核中连接硬件中断源与上层驱动处理程序的核心枢纽。为了在日益复杂的 SoC(片上系统)上统一管理来自不同中断控制器(如 GIC、APIC、IO-APIC 等)的中断,内核引入了三层抽象框架:中断控制器抽象层、IRQ Domain 和 中断描述符。理解这三层结构是进行中断注册与性能验证的基础。

3.1 三层框架总览

下图展示了中断从硬件引脚触发到 CPU 执行处理函数的完整路径,以及各抽象层在其中的位置。

Linux中断子系统三层框架 Linux 中断子系统三层框架 硬件中断源 GPIO 引脚 | 定时器 | 外设 DMA | 网络控制器 中断控制器抽象层 (irq_chip) GIC | APIC | 级联控制器 | 自定义控制器 IRQ Domain (irq_domain) 硬件 IRQ 号 → Linux IRQ 号 映射 中断描述符 (irq_desc) irqaction 链表 | 状态标志 | 线程化处理
关键理解:硬件中断号(hwirq)是物理概念,Linux IRQ 号是软件概念。IRQ Domain 负责将 hwirq 映射到 irq_desc 数组的索引,从而找到对应的中断处理逻辑。

3.2 中断控制器抽象层 (irq_chip)

irq_chip 是中断控制器的软件抽象,它封装了操作底层中断控制器硬件所需的全部回调函数。每个中断控制器实例(如 GIC-400、ARM Generic Timer)都对应一个 irq_chip 结构体。

回调函数 功能描述 典型实现
irq_mask 屏蔽指定中断源 写 GICD_ICENABLER 寄存器
irq_unmask 取消屏蔽中断源 写 GICD_ISENABLER 寄存器
irq_ack 应答中断,允许后续中断到达 读 GICC_IAR 寄存器
irq_eoi 中断处理结束,通知控制器 写 GICC_EOIR 寄存器
irq_set_type 设置中断触发方式(电平/边沿) 写 GICD_ICFGR 寄存器
irq_set_affinity 设置中断亲和性(绑定到指定 CPU) 写 GICD_ITARGETSR 寄存器

驱动开发者通常不直接操作 irq_chip,而是通过 request_irq() 或 request_threaded_irq() 注册中断处理函数。内核在底层自动调用 irq_chip 的方法完成硬件控制。

3.3 IRQ Domain:硬件中断号到 Linux IRQ 号的桥梁

在设备树(Device Tree)或 ACPI 中,设备使用硬件中断号(hwirq)描述中断连接。IRQ Domain 的核心职责是建立 hwirq → Linux IRQ 号 的映射关系。根据映射方式的不同,IRQ Domain 分为三种类型:

  • 线性映射 (Linear):预分配一个固定大小的数组,hwirq 直接作为数组索引。适用于 hwirq 范围小且连续的场景(如 GIC)。
  • 树形映射 (Tree):使用基数树(radix tree)存储映射关系。适用于 hwirq 范围大且稀疏的场景(如 GPIO 中断控制器)。
  • 不映射 (No Map):hwirq 与 Linux IRQ 号相同,或由控制器自行管理。适用于简单或遗留系统。
IRQ Domain 映射关系 IRQ Domain 映射关系 硬件中断号 (hwirq) hwirq 0: 定时器 hwirq 1: UART hwirq 2: DMA hwirq 3: GPIO ... IRQ Domain 线性 / 树形 / 不映射 irq_domain_ops .map / .unmap / .xlate Linux IRQ 号 irq 32: 定时器 irq 33: UART irq 34: DMA irq 35: GPIO ...
性能要点:线性映射的查找时间复杂度为 O(1),树形映射为 O(log n)。在中断密集型场景(如高速网络设备),线性映射能减少中断响应延迟。设备树中通过 interrupt-parent 和 interrupts 属性指定 IRQ Domain 和 hwirq。

3.4 中断描述符 (irq_desc)

每个 Linux IRQ 号对应一个 irq_desc 结构体,它是中断处理的核心管理单元。内核维护一个全局的 irq_desc 数组(或基数树),通过 IRQ 号索引。

irq_desc 的主要字段包括:

  • irq_data:指向 irq_chip 和 irq_domain 的指针,以及硬件中断号。
  • action:指向 irqaction 链表的头指针。一个中断可以共享,多个 irqaction 通过链表串联。
  • status_use_accessors:中断状态标志,如 IRQ_DISABLED、IRQ_INPROGRESS、IRQ_WAKE_THREAD。
  • threads_oneshot:用于线程化中断处理,确保中断线程执行完毕前不会再次触发。
  • depth:中断屏蔽深度计数,用于嵌套屏蔽/取消屏蔽操作。
// 内核源码 include/linux/irqdesc.h (简化结构)
struct irq_desc {
    struct irq_common_data  irq_common_data;
    struct irq_data         irq_data;
    unsigned int __percpu   *kstat_irqs;
    struct irqaction        *action;        /* 中断处理函数链表 */
    unsigned int            status_use_accessors;
    unsigned int            core_internal_state__do_not_mess_with_it;
    unsigned int            depth;          /* 嵌套屏蔽深度 */
    unsigned int            wake_depth;     /* 唤醒使能深度 */
    unsigned int            irq_count;      /* 中断计数 */
    unsigned long           last_unhandled; /* 上次未处理时间戳 */
    unsigned int            irqs_unhandled; /* 未处理中断计数 */
    struct task_struct      *thread;        /* 中断线程 */
    struct irqaction        *threads_oneshot;
    raw_spinlock_t          lock;
    struct cpumask          *percpu_enabled;
    const char              *name;
} ____cacheline_internodealigned_in_smp;
避坑指南:在共享中断中,irq_desc->action 链表上的每个 irqaction 都必须调用 irqreturn_t 返回 IRQ_NONE 或 IRQ_HANDLED。如果所有处理函数都返回 IRQ_NONE,内核会认为该中断是“未处理中断”,累计超过阈值后可能触发 spurious_interrupt 处理,甚至禁用该中断。

3.5 三层协作流程:从硬件中断到驱动回调

当硬件中断触发时,三层框架按以下顺序协作:

  1. 硬件层:中断控制器(如 GIC)检测到中断信号,将中断 ID 推送到 CPU 接口。
  2. 中断控制器抽象层:CPU 执行架构相关的异常向量,调用 handle_arch_irq,该函数读取 GICC_IAR 获取 hwirq,然后调用 irq_chip->irq_ack() 应答中断。
  3. IRQ Domain 层:通过 irq_find_mapping() 将 hwirq 转换为 Linux IRQ 号。
  4. 中断描述符层:根据 IRQ 号找到 irq_desc,调用 generic_handle_irq_desc(),遍历 action 链表执行注册的 irq_handler_t 回调函数。
  5. 结束处理:调用 irq_chip->irq_eoi() 通知控制器中断处理完成。
中断处理完整流程 中断处理完整流程 硬件中断触发 irq_chip.ack() irq_find_mapping() 执行 action 回调 irq_chip.eoi() 中断返回

3.6 性能验证关注点

在进行中断性能验证时,需要重点关注以下与框架相关的指标:

关注点 影响 验证方法
IRQ Domain 映射延迟 线性映射 vs 树形映射的查找时间差异 使用 perf probe 探测 irq_find_mapping 函数耗时
中断描述符锁竞争 多核并发访问同一 irq_desc 的 spinlock 使用 lock_stat 或 ftrace 跟踪锁等待时间
中断线程化调度延迟 线程化中断处理受 CFS 调度器影响 测量 irq/xxx 内核线程的调度延迟分布
中断亲和性设置 错误绑定导致跨 CPU 缓存失效 使用 mpstat 观察各 CPU 中断分布
调试命令:使用 cat /proc/interrupts 查看每个 CPU 的中断计数和亲和性;使用 cat /proc/irq/<IRQ号>/smp_affinity 查看或修改中断亲和性掩码。