3. Linux 中断子系统架构:分层、irq_chip、irq_domain 与中断描述符
中断子系统是 Linux 内核中连接硬件与驱动软件的核心枢纽。为了在 x86、ARM、RISC-V 等不同架构上实现统一的驱动接口,内核将中断处理抽象为分层架构,并通过 irq_chip、irq_domain 和 中断描述符 三个关键概念完成硬件中断号到 Linux IRQ 号的映射与管理。
核心认知: 驱动开发者通常只与“Linux IRQ 号”和“中断处理函数”打交道,而中断子系统底层负责将硬件管脚上的电气信号,安全、高效地转化为内核可调度的执行流。
3.1 中断子系统分层架构
Linux 中断子系统从上到下分为三层,每一层职责清晰,通过标准接口解耦:
| 层次 | 核心组件 | 主要职责 | 驱动开发者关注点 |
|---|---|---|---|
| 通用中断层 (Generic IRQ) | irq_desc、request_irq()、handle_irq() |
维护中断描述符、管理中断流控类型 (边缘/电平)、调度执行 handler | 调用 request_irq() / devm_request_irq() 注册 handler |
| 中断芯片层 (irq_chip) | struct irq_chip |
封装硬件中断控制器 (GIC、IOAPIC、GPIO) 的底层操作 | 实现 irq_chip 回调 (mask/unmask/ack) |
| 中断域层 (irq_domain) | struct irq_domain |
将硬件中断源 (hwirq) 映射到 Linux IRQ 号 | 在设备树或 ACPI 中描述中断连接,注册 irq_domain |
分层价值: 更换 SoC 或中断控制器时,只需替换
irq_chip 和 irq_domain 的实现,上层的通用中断层和驱动代码无需修改。
3.2 irq_chip:硬件中断控制器的软件抽象
struct irq_chip 是内核为中断控制器定义的通用接口结构体。每个中断控制器 (如 GICv2、GICv3、IOAPIC) 都需要实例化该结构体,并实现必要的回调函数。
// include/linux/irq.h
struct irq_chip {
const char *name; // 芯片名称,用于 /proc/interrupts
unsigned int (*irq_mask)(struct irq_data *data); // 屏蔽中断
unsigned int (*irq_unmask)(struct irq_data *data); // 取消屏蔽
void (*irq_ack)(struct irq_data *data); // 应答中断 (边缘触发)
void (*irq_eoi)(struct irq_data *data); // 中断结束 (电平触发)
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
// ... 其他可选回调
};
| 回调函数 | 触发场景 | 典型实现 |
|---|---|---|
irq_mask |
中断被禁用或 CPU 进入休眠 | 写中断控制器屏蔽寄存器 |
irq_unmask |
中断被重新使能 | 写中断控制器使能寄存器 |
irq_ack |
边缘触发中断到达,CPU 自动调用 | 写中断确认寄存器,清除挂起位 |
irq_eoi |
电平触发中断处理完成后 | 写中断结束寄存器,允许新中断 |
irq_set_type |
驱动调用 irq_set_irq_type() |
配置触发方式 (上升沿/高电平) |
常见陷阱: 边缘触发中断必须实现
irq_ack,电平触发中断必须实现 irq_eoi。混淆两者会导致中断丢失或死锁。
3.3 irq_domain:硬件中断号到 Linux IRQ 号的桥梁
在设备树或 ACPI 时代,硬件中断源 (hwirq) 是动态的,无法预分配 Linux IRQ 号。irq_domain 负责在运行时建立 hwirq → Linux IRQ 的映射关系。
三种常见的映射方式:
- 线性映射 (Linear):适用于 hwirq 编号连续且数量固定的控制器 (如 GIC-400)。查找速度快,O(1) 复杂度。
- 树形映射 (Tree):适用于 hwirq 稀疏或不连续的场景 (如 GPIO 中断控制器)。使用基数树存储,节省内存。
- 不映射 (No Map):由驱动手动指定 IRQ 号,仅用于 x86 等固定中断号的 legacy 系统。
// 设备树中描述中断连接示例 (ARM GIC)
gic: interrupt-controller@f0001000 {
compatible = "arm,gic-400";
interrupt-controller;
#interrupt-cells = <3>; // <中断类型 中断号 触发方式>
};
uart0: serial@f0002000 {
compatible = "arm,pl011";
interrupts = <0 33 4>; // SPI, hwirq=33, 高电平触发
interrupt-parent = <&gic>;
};
内核解析设备树时,irq_domain 会为 uart0 分配一个 Linux IRQ 号 (如 45),驱动只需使用该 IRQ 号调用 request_irq()。
3.4 中断描述符 (irq_desc)
每个 Linux IRQ 号对应一个 struct irq_desc,它是中断子系统的核心数据结构,包含了该中断的所有状态信息。
// kernel/irq/internals.h (简化)
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data; // 指向 irq_chip 和 irq_domain
unsigned int __percpu *kstat_irqs; // 统计信息
irq_flow_handler_t handle_irq; // 流控处理函数
struct irqaction *action; // 链表,挂载所有 handler
unsigned int status_use_accessors;
struct proc_dir_entry *dir; // /proc/irq/ 目录
const char *name; // 中断名称
};
| 字段 | 作用 | 驱动关联 |
|---|---|---|
handle_irq |
中断流控处理函数 (边缘/电平/简单) | 由 irq_chip 和触发类型决定 |
action |
指向 irqaction 链表的头节点 |
request_irq() 创建的 handler 挂载在此 |
irq_data |
包含 irq_chip 和 irq_domain 指针 |
驱动通过 irq_get_irq_data() 访问 |
kstat_irqs |
每个 CPU 的中断计数 | /proc/interrupts 显示的数据来源 |
调试技巧: 查看
/proc/interrupts 可以确认中断是否被正确注册、触发次数以及 CPU 亲和性。如果某个中断计数不增长,说明硬件未触发或 irq_domain 映射错误。
3.5 全流程协同:从硬件中断到驱动 handler
当硬件中断信号到达 CPU 时,中断子系统按以下流程协同工作:
- 硬件触发:中断控制器检测到管脚电平/边沿变化,向 CPU 发送 IRQ 信号。
- CPU 异常入口:CPU 保存上下文,跳转到
arch_irq_handler汇编入口。 - irq_domain 查找:根据硬件中断号 (hwirq) 通过
irq_domain找到对应的 Linux IRQ 号。 - irq_desc 调度:获取
irq_desc,调用handle_irq流控函数 (如handle_edge_irq)。 - irq_chip 操作:流控函数调用
irq_chip的ack/mask/eoi方法操作硬件。 - 执行 handler:遍历
action链表,依次执行驱动注册的中断处理函数。
设计要点: 中断处理函数 (handler) 运行在中断上下文中,必须是原子的,不能睡眠、不能获取信号量、不能调用可能导致调度的函数。耗时操作应使用 中断下半部 (tasklet/workqueue/threaded irq) 处理。