3. Linux 中断子系统架构
Linux 中断子系统是内核中管理硬件中断请求(IRQ)的核心模块,负责将外设的中断信号安全、高效地传递到对应的驱动程序处理函数。整个子系统采用分层架构设计,从下至上依次为:硬件层、核心层和驱动层。每一层职责清晰,通过标准接口进行交互,使得驱动开发者无需关心底层中断控制器的具体实现细节。
架构核心思想: 中断子系统通过抽象中断控制器(irq_chip)和中断描述符(irq_desc),实现了硬件无关性与驱动可移植性。驱动层只需调用
request_irq() / devm_request_irq() 注册中断处理函数,核心层负责路由与分发,硬件层负责与具体中断控制器(如 GIC、APIC、NVIC)交互。
3.1 中断子系统分层详解
中断子系统的三层结构各司其职,通过标准化的数据结构与函数接口进行协作。下表总结了每一层的核心职责、关键数据结构和主要接口函数。
| 分层 | 核心职责 | 关键数据结构 | 主要接口 / 函数 |
|---|---|---|---|
| 硬件层 | 直接操作中断控制器寄存器;处理 CPU 异常入口;完成硬件中断信号的接收与应答 | 中断控制器寄存器映射、中断向量表 | irq_chip 回调(.irq_mask、.irq_unmask、.irq_ack、.irq_set_type) |
| 核心层 | 管理中断描述符;实现中断流控;维护 irq_domain 映射;提供通用中断控制接口 | struct irq_desc、struct irq_domain、struct irq_chip |
handle_irq()、generic_handle_irq()、irq_find_mapping() |
| 驱动层 | 注册中断处理函数;实现设备特定的中断逻辑;管理中断使能与禁用 | 中断处理函数指针、设备上下文数据 | request_irq()、devm_request_irq()、free_irq()、enable_irq()、disable_irq() |
开发提示: 驱动开发者通常只需要与驱动层接口打交道。核心层和硬件层的细节由内核维护者或 BSP 工程师完成。理解分层结构有助于在调试中断问题时快速定位问题所在层。
3.2 中断描述符(irq_desc)结构
struct irq_desc 是中断子系统的核心数据结构,每个硬件中断源(IRQ)都对应一个 irq_desc 实例。它串联了硬件层的中断控制器操作、核心层的流控处理以及驱动层的中断处理函数。内核维护一个全局的 irq_desc 数组(或基数树),通过 IRQ 号索引。
3.2.1 irq_desc 核心字段解析
以下列出 struct irq_desc 中最关键的几个字段,理解它们有助于掌握中断的完整处理流程。
- irq_data:包含
irq_chip *chip(指向硬件操作函数集)和irq_domain *domain(用于硬件 IRQ 号到 Linux IRQ 号的映射)。 - handle_irq:中断流控函数指针,内核根据中断类型(电平、边沿、NMI 等)在初始化时设置为
handle_level_irq、handle_edge_irq或handle_fasteoi_irq等。 - action:指向
struct irqaction链表的头指针。每个irqaction包含驱动注册的处理函数handler、设备 IDdev_id、中断名称name等。多个驱动可以共享同一个 IRQ 号(如 PCI 设备共享中断)。 - status / istate:中断状态标志位,如
IRQ_DISABLED、IRQ_INPROGRESS、IRQ_WAITING等,用于内核内部状态管理。 - depth:中断禁用深度计数器,每次调用
disable_irq()递增,enable_irq()递减,为 0 时中断才真正使能。
注意事项: 在共享中断(IRQF_SHARED)场景下,内核会遍历
action 链表,依次调用每个注册的处理函数。驱动必须通过 dev_id 参数判断中断是否来自自己的设备,并在不匹配时立即返回 IRQ_NONE。
3.2.2 irq_desc 生命周期与初始化流程
中断描述符的初始化发生在系统启动早期,由 early_irq_init() 和 init_IRQ() 完成。以下是典型初始化步骤:
- 静态分配或动态分配:内核通过
irq_desc_alloc()分配irq_desc实例,通常以数组形式静态分配(NR_IRQS 大小),或使用基数树动态扩展。 - 设置默认流控函数:将
handle_irq初始化为handle_bad_irq,后续由中断控制器驱动根据硬件特性替换为正确的流控函数。 - 绑定 irq_chip:通过
irq_set_chip()将irq_data->chip指向具体的中断控制器操作集(如 GIC、APIC 的irq_chip结构体)。 - 设置 irq_domain 映射:通过
irq_domain建立硬件中断号(hwirq)到 Linux IRQ 号的映射关系。 - 驱动注册中断:驱动调用
request_irq()时,内核将irqaction挂入irq_desc->action链表,并最终使能中断。
3.3 中断处理路径:从硬件到驱动
当中断信号从外设到达 CPU 时,完整的处理路径如下:
- 硬件层:中断控制器(如 GIC)检测到中断,向 CPU 发送 IRQ 信号。CPU 保存上下文,跳转到中断向量表对应的入口地址。
- 核心层入口:汇编入口代码调用
handle_arch_irq()或generic_handle_irq(),根据 IRQ 号获取对应的irq_desc。 - 流控处理:调用
irq_desc->handle_irq(如handle_level_irq),执行中断屏蔽、应答、清除等硬件操作,并调用handle_irq_event()。 - 驱动处理:
handle_irq_event()遍历irq_desc->action链表,依次执行每个irqaction->handler(即驱动注册的中断处理函数)。 - 结束处理:流控函数执行中断取消屏蔽(unmask)或 EOI(End Of Interrupt)操作,恢复 CPU 中断状态。
关键代码路径示例: 驱动注册中断的典型代码片段如下:
/* 驱动中断处理函数 */
static irqreturn_t my_device_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
/* 读取设备状态寄存器,判断中断来源 */
uint32_t status = ioread32(dev->regs + STATUS_REG);
if (status & RX_READY) {
/* 处理接收数据 */
...
return IRQ_HANDLED;
}
return IRQ_NONE; /* 非本设备中断 */
}
/* 驱动初始化时注册中断 */
static int my_device_probe(struct platform_device *pdev)
{
struct my_device *dev;
int irq, ret;
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
/* 注册中断,IRQF_TRIGGER_RISING 表示上升沿触发 */
ret = devm_request_irq(&pdev->dev, irq, my_device_irq_handler,
IRQF_TRIGGER_RISING | IRQF_SHARED,
"my_device", dev);
if (ret) {
dev_err(&pdev->dev, "failed to request irq %d\n", irq);
return ret;
}
return 0;
}
3.4 中断描述符与 irq_domain 映射
在现代嵌入式系统中,中断控制器可能包含大量中断源(如 GIC-400 支持 1024 个 SPI 中断)。irq_domain 是核心层提供的一种映射机制,用于将硬件中断号(hwirq)转换为Linux IRQ 号,并关联到对应的 irq_desc。常见的映射类型包括:
| 映射类型 | 描述 | 适用场景 |
|---|---|---|
| 线性映射 (linear) | hwirq 直接作为数组索引,映射速度最快 | 中断源数量固定且较少(如 < 256) |
| 树形映射 (tree) | 使用基数树(radix tree)存储映射关系 | 中断源数量大且稀疏(如 GIC 的 SPI) |
| 不映射 (no-map) | hwirq 直接等于 Linux IRQ 号 | 简单嵌入式系统,中断号固定 |
调试技巧: 可以通过
/proc/interrupts 查看当前系统中所有注册的中断描述符信息,包括 IRQ 号、CPU 亲和性、中断次数以及注册的驱动名称。这是排查中断未触发或触发异常的首选工具。