4. Linux 中断子系统架构:中断描述符、irq_chip、irq_domain、中断流处理层
Linux 内核的中断子系统是连接硬件外设与上层驱动软件的桥梁。对于 GPIO 驱动开发者而言,理解中断子系统的核心组件——中断描述符、irq_chip、irq_domain 以及中断流处理层——是编写高效、可移植中断处理程序的关键。本章将深入剖析这四大组件,揭示它们如何协同工作,将物理 GPIO 引脚上的电平变化转化为内核可识别的中断事件。
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
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 和触发类型。
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 引脚上产生中断时,整个中断子系统的协同工作流程如下:
- 硬件触发: GPIO 引脚电平变化,中断控制器检测到事件,将中断信号发送给 CPU。
- CPU 入口: CPU 保存上下文,跳转到内核中断入口向量表,进入
do_IRQ()函数。 - 查找 irq_desc:
do_IRQ()根据中断号获取对应的irq_desc实例。 - 调用流处理函数: 执行
irq_desc->handle_irq(),例如handle_edge_irq()。 - 流处理函数操作 irq_chip: 流处理函数调用
irq_chip的irq_ack()确认中断,必要时调用irq_mask()屏蔽中断。 - 执行驱动处理函数: 流处理函数调用
handle_irq_event(),遍历 action 链表,执行驱动注册的顶半部处理函数。 - 清理与恢复: 流处理函数调用
irq_unmask()取消屏蔽(对于电平触发),或等待 EOI(对于 fasteoi 类型),然后返回。
4.6 本章小结
Linux 中断子系统的四大组件构成了一个清晰的分层架构:
- 中断描述符(irq_desc): 每个中断的“身份证”,记录状态、处理函数和硬件接口。
- irq_chip: 硬件控制层的抽象,驱动通过实现回调函数来操作中断控制器寄存器。
- irq_domain: 硬件中断号与 Linux 中断号之间的映射管理器,是设备树中断资源解析的核心。
- 中断流处理层: 中断分发策略的实现者,根据触发类型选择不同的处理流程,并最终调用驱动注册的处理函数。
irq_chip 回调函数的正确实现(尤其是 irq_set_type 和 irq_ack),以及 irq_domain 的映射设置。流处理函数的选择直接影响中断行为的正确性——边沿触发使用 handle_edge_irq,电平触发使用 handle_level_irq,切勿混淆。