4. Linux 中断子系统架构:中断描述符、irq_chip、irq_domain、中断流处理层

Linux 内核的中断子系统是连接硬件外设与上层驱动软件的桥梁。对于 GPIO 驱动开发者而言,理解中断子系统的核心组件——中断描述符、irq_chip、irq_domain 以及中断流处理层——是编写高效、可移植中断处理程序的关键。本章将深入剖析这四大组件,揭示它们如何协同工作,将物理 GPIO 引脚上的电平变化转化为内核可识别的中断事件。

核心要点:中断子系统采用分层抽象设计,将硬件相关的控制器操作与硬件无关的中断分发逻辑解耦。GPIO 驱动主要工作在 irq_chip 和 irq_domain 层面,而中断流处理层则负责调用驱动注册的顶半部(Top Half)处理函数。

4.1 中断描述符(irq_desc)

中断描述符是内核中描述一个中断源的核心数据结构。每个中断号(IRQ number)都对应一个 struct irq_desc 实例,它包含了该中断的所有状态信息、处理函数指针以及底层硬件控制接口。

字段 类型 描述
irq_data struct irq_data 包含中断号、irq_chip 指针、irq_domain 私有数据等核心信息
handle_irq irq_flow_handler_t 中断流处理函数指针,决定中断的响应方式(如边沿触发、电平触发)
action struct irqaction * 指向中断处理动作链表的头节点,驱动通过 request_irq() 注册的处理函数挂在此链表上
status unsigned int 中断状态标志位,如 IRQ_DISABLED、IRQ_INPROGRESS 等
depth unsigned int 中断禁用深度计数,用于嵌套的 disable_irq() / enable_irq() 调用

中断描述符的组织形式是一个全局数组 irq_desc[](在 SPARSE_IRQ 开启时变为基数树),内核通过中断号直接索引到对应的描述符。驱动开发者通常不直接操作 irq_desc,而是通过 irq_data 和 irq_chip 提供的接口间接访问。

调试技巧:在 /proc/interrupts 文件中,每一行对应一个中断描述符。你可以通过该文件查看中断号、CPU 亲和性、触发次数以及注册的驱动名称,是排查中断未触发或丢失问题的第一站。

4.2 irq_chip:硬件控制接口

irq_chip 是中断控制器硬件的软件抽象。它定义了一组回调函数,用于操作底层中断控制器寄存器,例如屏蔽/取消屏蔽中断、设置触发类型、确认中断(ACK)等。GPIO 控制器驱动通常需要实现自己的 irq_chip,以将 GPIO 引脚的中断功能暴露给内核。

struct irq_chip gpio_irq_chip = {
    .name           = "gpio_irq",
    .irq_mask       = gpio_irq_mask,       // 屏蔽中断
    .irq_unmask     = gpio_irq_unmask,     // 取消屏蔽
    .irq_set_type   = gpio_irq_set_type,   // 设置触发类型(上升沿/下降沿/高电平/低电平)
    .irq_ack        = gpio_irq_ack,        // 确认中断(清除挂起状态)
    .irq_bus_lock   = gpio_irq_bus_lock,   // 慢速总线锁(如 I2C/SPI GPIO 扩展芯片)
    .irq_bus_sync_unlock = gpio_irq_bus_sync_unlock, // 同步解锁并提交寄存器配置
};

关键回调函数说明:

  • irq_mask / irq_unmask: 控制中断的启用与禁用,通常在中断流处理层进入临界区时调用。
  • irq_set_type: 设置中断触发条件,GPIO 驱动需要根据硬件寄存器配置上升沿、下降沿或电平触发。
  • irq_ack: 确认中断,告诉硬件已收到中断信号,通常用于边沿触发中断以清除挂起位。
  • irq_bus_lock / irq_bus_sync_unlock: 对于挂接在慢速总线(如 I2C)上的 GPIO 扩展芯片,用于批量提交寄存器配置,避免频繁总线通信。
常见错误:在实现 irq_set_type 时,必须同时更新 irq_data 中的 trigger_type 字段,否则内核无法正确识别中断触发方式,可能导致中断无法触发或误触发。

4.3 irq_domain:中断号映射器

irq_domain 负责将硬件中断号(hwirq)映射到Linux 中断号(virq)。对于 GPIO 控制器,每个 GPIO 引脚都有一个硬件中断号(通常是引脚编号),而 irq_domain 负责为这些引脚分配并管理对应的 Linux 中断号。这种映射机制使得设备树(Device Tree)或 ACPI 中描述的硬件中断资源能够被内核统一管理。

映射类型:irq_domain 支持三种映射方式:线性映射(Linear)、树形映射(Tree)和不映射(No Map)。GPIO 控制器通常使用线性映射,因为引脚编号通常是连续的。
// 创建线性映射的 irq_domain
struct irq_domain *gpio_irq_domain;
gpio_irq_domain = irq_domain_add_linear(
    dev->of_node,           // 设备树节点
    gpio_chip->ngpio,       // GPIO 引脚数量
    &gpio_irq_domain_ops,   // 域操作函数集
    gpio_chip               // 私有数据
);

irq_domain 的核心操作函数集 struct irq_domain_ops 包含:

  • map: 将硬件中断号映射到 Linux 中断号,初始化对应的 irq_desc 和 irq_chip。
  • unmap: 解除映射关系,释放资源。
  • xlate: 将设备树中的中断描述(如 <&gpio0 5 IRQ_TYPE_EDGE_RISING>)转换为 hwirq 和触发类型。
irq_domain 映射流程 硬件中断号 hwirq = 5 (GPIO Pin 5) irq_domain 线性映射 / 树形映射 xlate() → map() Linux 中断号 virq = 42 映射过程说明 1. 设备树解析器调用 xlate(),将 <&gpio0 5 IRQ_TYPE_EDGE_RISING> 转换为 hwirq=5, type=上升沿 2. irq_domain 调用 map(),分配 virq=42,并关联 irq_chip 和 irq_desc 3. 驱动通过 virq=42 调用 request_irq() 注册处理函数

4.4 中断流处理层(Interrupt Flow Handler)

中断流处理层是中断子系统的分发中心。当 CPU 接收到硬件中断信号后,会跳转到内核的通用中断入口代码,最终调用 irq_desc 中的 handle_irq 函数指针。这个函数指针指向一个中断流处理函数,它根据中断类型(边沿触发、电平触发等)执行不同的处理逻辑。

常见的中断流处理函数:

流处理函数 适用场景 行为特点
handle_simple_irq() 无需硬件 ACK 的中断 直接调用 action 链表中的处理函数,不执行 mask/ack 操作
handle_level_irq() 电平触发中断 进入中断后立即 mask 中断,防止重复触发;处理完成后 unmask
handle_edge_irq() 边沿触发中断 进入中断后 ack 清除挂起位,允许后续边沿再次触发;处理期间新边沿会被挂起
handle_fasteoi_irq() 支持 EOI 的中断控制器 不执行 mask,直接调用处理函数,然后调用 chip->irq_eoi() 通知控制器中断处理完毕

对于 GPIO 驱动,最常用的是 handle_level_irq() 和 handle_edge_irq()。驱动在 irq_domain->ops->map() 中通过 irq_set_handler() 设置合适的流处理函数:

static int gpio_irq_domain_map(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw)
{
    struct gpio_chip *gc = d->host_data;

    // 设置 irq_chip
    irq_set_chip_data(virq, gc);
    irq_set_chip(virq, &gpio_irq_chip);

    // 设置中断流处理函数:边沿触发使用 handle_edge_irq,电平触发使用 handle_level_irq
    irq_set_handler(virq, handle_edge_irq);

    // 设置触发类型为默认值(通常由设备树覆盖)
    irq_set_irq_type(virq, IRQ_TYPE_EDGE_RISING);

    return 0;
}
流处理层与顶半部:中断流处理函数在执行过程中会调用 __do_irq(),进而遍历 action 链表,调用驱动通过 request_irq() 注册的顶半部处理函数。顶半部执行时中断是关闭的(取决于流处理函数实现),因此必须快速执行,耗时操作应推迟到底半部(Bottom Half)处理。

4.5 四大组件协同工作流程

当 GPIO 引脚上产生中断时,整个中断子系统的协同工作流程如下:

  1. 硬件触发: GPIO 引脚电平变化,中断控制器检测到事件,将中断信号发送给 CPU。
  2. CPU 入口: CPU 保存上下文,跳转到内核中断入口向量表,进入 do_IRQ() 函数。
  3. 查找 irq_desc: do_IRQ() 根据中断号获取对应的 irq_desc 实例。
  4. 调用流处理函数: 执行 irq_desc->handle_irq(),例如 handle_edge_irq()。
  5. 流处理函数操作 irq_chip: 流处理函数调用 irq_chip 的 irq_ack() 确认中断,必要时调用 irq_mask() 屏蔽中断。
  6. 执行驱动处理函数: 流处理函数调用 handle_irq_event(),遍历 action 链表,执行驱动注册的顶半部处理函数。
  7. 清理与恢复: 流处理函数调用 irq_unmask() 取消屏蔽(对于电平触发),或等待 EOI(对于 fasteoi 类型),然后返回。
中断子系统协同工作流程 ① GPIO 硬件触发中断 ② CPU 入口 do_IRQ() ③ 查找 irq_desc ④ 调用流处理函数 ⑤ 操作 irq_chip ⑥ 执行驱动处理函数 ⑦ 清理与恢复

4.6 本章小结

Linux 中断子系统的四大组件构成了一个清晰的分层架构:

  • 中断描述符(irq_desc): 每个中断的“身份证”,记录状态、处理函数和硬件接口。
  • irq_chip: 硬件控制层的抽象,驱动通过实现回调函数来操作中断控制器寄存器。
  • irq_domain: 硬件中断号与 Linux 中断号之间的映射管理器,是设备树中断资源解析的核心。
  • 中断流处理层: 中断分发策略的实现者,根据触发类型选择不同的处理流程,并最终调用驱动注册的处理函数。
实践要点:在编写 GPIO 驱动中断部分时,重点关注 irq_chip 回调函数的正确实现(尤其是 irq_set_type 和 irq_ack),以及 irq_domain 的映射设置。流处理函数的选择直接影响中断行为的正确性——边沿触发使用 handle_edge_irq,电平触发使用 handle_level_irq,切勿混淆。