2. Linux中断子系统架构:从硬件中断到CPU处理的全链路分析
中断子系统是Linux内核与硬件交互的核心枢纽。本章将沿着中断信号从外设引脚到CPU执行上下文的完整路径,逐层剖析中断控制器、CPU接口、内核入口、中断分发与执行的全链路架构。
2.1 中断子系统分层模型
Linux中断子系统采用清晰的分层设计,每一层负责不同的抽象与硬件交互。下表总结了各层的主要职责与核心数据结构:
| 层次 | 职责 | 核心数据结构/接口 |
|---|---|---|
| 硬件层 | 外设产生中断信号,中断控制器仲裁与路由 | GIC、APIC、IO-APIC 寄存器 |
| 中断控制器抽象层 | 封装不同中断控制器的操作,提供统一接口 | struct irq_chip |
| 中断号映射层 | 将硬件中断号映射为Linux IRQ号 | struct irq_domain |
| 中断描述符层 | 管理每个IRQ的状态、处理函数、线程化信息 | struct irq_desc |
| 中断处理入口层 | CPU异常向量表入口,保存/恢复上下文 | arch/arm64/kernel/entry.S |
| 通用中断处理层 | 调用中断描述符链上的处理函数,处理线程化 | handle_irq_desc() |
关键概念: 硬件中断号(hwirq)与Linux IRQ号是两套独立的编号体系。irq_domain 负责在两者之间建立映射关系,这是设备树与ACPI中断解析的核心机制。
2.2 全链路数据流与核心组件
下图展示了从外设触发中断到CPU执行中断服务例程的完整数据流,以及各阶段涉及的关键组件:
2.3 中断控制器抽象:struct irq_chip
irq_chip 是中断控制器硬件操作的抽象层。每个中断控制器驱动(如GICv3、APIC)都需要实现该结构体中的回调函数。核心回调包括:
- irq_ack:应答中断,通常发生在中断处理早期,防止同一中断再次触发。
- irq_mask / irq_unmask:屏蔽/取消屏蔽特定IRQ,用于流控与线程化。
- irq_set_type:设置中断触发方式(电平触发、边沿触发)。
- irq_set_affinity:设置中断亲和性,将中断绑定到指定CPU核心。
实战提示: 在编写中断控制器驱动时,务必实现 irq_ack 与 irq_mask/unmask。缺失 irq_ack 会导致中断无法被正确应答,系统可能陷入无限中断循环。
2.4 中断号映射:irq_domain
irq_domain 是现代Linux内核中管理硬件中断号到Linux IRQ号映射的核心机制。它支持三种映射方式:
| 映射类型 | 描述 | 适用场景 |
|---|---|---|
| 线性映射 (Linear) | 硬件中断号直接作为Linux IRQ号的偏移 | 中断号连续且数量固定的控制器 |
| 树形映射 (Tree) | 使用基数树存储映射关系,支持稀疏中断号 | 中断号不连续或数量庞大的系统 |
| 不映射 (No Map) | 由驱动自行管理IRQ号的分配 | 特殊硬件或虚拟中断控制器 |
设备树中的中断描述通过 interrupts 属性指定硬件中断号,内核在初始化时调用 irq_domain_alloc_irqs() 完成映射:
// 设备树示例:外设中断描述
uart@fe000000 {
compatible = "vendor,uart";
reg = <0x0 0xfe000000 0x0 0x1000>;
interrupts = <0 33 4>; // GIC SPI 33, 高电平触发
};
// 内核映射过程(简化)
struct irq_domain *domain = irq_find_host(dev->of_node);
unsigned int virq = irq_create_mapping(domain, hwirq);
ret = request_irq(virq, my_handler, 0, "my_device", dev);
2.5 中断描述符:irq_desc 内部结构
每个注册的IRQ都对应一个 struct irq_desc 实例,它是中断管理的核心数据结构。其关键字段如下:
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs; // 中断统计
irq_flow_handler_t handle_irq; // 流控处理函数
struct irqaction *action; // 处理函数链表
unsigned int status_use_accessors; // 中断状态
struct task_struct *thread; // 中断线程化任务
struct cpumask *percpu_enabled; // CPU亲和性掩码
...
};
注意: irq_desc 中的 action 链表是实现共享中断的关键。多个驱动可以注册同一个IRQ号,内核在中断发生时依次调用链表上的所有处理函数。每个处理函数必须检查中断是否来自自己的设备,若不是则立即返回 IRQ_NONE。
2.6 中断处理入口与通用处理层
当CPU收到中断信号后,硬件自动跳转到异常向量表中对应的入口。以ARM64为例,中断入口在 arch/arm64/kernel/entry.S 中定义:
// ARM64 中断入口(简化)
el1_irq:
// 保存上下文
stp x29, x30, [sp, #-16]!
// 调用 C 函数
bl handle_arch_irq
// 恢复上下文
ldp x29, x30, [sp], #16
eret
// handle_arch_irq 最终调用到:
void handle_irq_desc(struct irq_desc *desc) {
// 调用流控处理函数
desc->handle_irq(desc);
// 流控函数内部会调用 __handle_irq_event_percpu()
// 进而遍历 action 链表,执行每个注册的处理函数
}
通用中断处理层 __handle_irq_event_percpu() 负责遍历 action 链表,依次调用每个处理函数,并处理线程化中断的唤醒逻辑:
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc) {
irqreturn_t retval = IRQ_NONE;
struct irqaction *action;
for_each_action_of_desc(desc, action) {
irqreturn_t res;
// 调用注册的处理函数
res = action->handler(irq, action->dev_id);
if (res == IRQ_WAKE_THREAD)
// 唤醒中断线程
wake_up_process(action->thread);
retval |= res;
}
return retval;
}
2.7 中断线程化机制
中断线程化是Linux内核减少关中断时间、提高实时性的重要机制。其核心思想是将中断处理分为两部分:
- 上半部(硬中断上下文):在关中断或屏蔽中断的状态下执行,只做最必要的硬件操作(如应答中断、读取状态寄存器),然后返回 IRQ_WAKE_THREAD。
- 下半部(线程上下文):内核唤醒一个内核线程执行剩余的处理逻辑,此时中断已重新使能,可以执行耗时操作(如数据拷贝、协议处理)。
线程化中断的注册方式:
// 注册线程化中断
request_threaded_irq(irq,
hard_handler, // 上半部,可为 NULL
thread_fn, // 下半部线程函数
IRQF_SHARED, // 支持共享中断
"my_driver", dev_id);
// 当 hard_handler 返回 IRQ_WAKE_THREAD 时,内核唤醒线程执行 thread_fn
性能考量: 线程化中断将处理函数移出硬中断上下文,允许其他中断被响应,显著降低了中断延迟抖动。但线程切换本身有开销,对于高频中断(如网络收包),建议仍使用传统上半部或使用 NAPI 轮询。
2.8 共享中断实战要点
共享中断允许多个设备共用同一个IRQ号,在嵌入式系统和PCI总线中极为常见。实现共享中断必须遵守以下规则:
- 注册时指定 IRQF_SHARED 标志,否则内核会拒绝共享。
- 每个处理函数必须检查中断是否来自自己的设备,通过读取设备状态寄存器判断。
- 处理函数必须返回 IRQ_NONE 或 IRQ_HANDLED,内核根据返回值判断中断是否被正确处理。
- 所有共享该IRQ的驱动必须使用相同的触发方式(电平或边沿),否则会导致中断丢失或风暴。
共享中断的典型实现模式:
static irqreturn_t my_shared_handler(int irq, void *dev_id) {
struct my_device *dev = (struct my_device *)dev_id;
u32 status;
// 读取设备中断状态寄存器
status = readl(dev->base + INT_STATUS);
if (!(status & MY_DEVICE_INT_PENDING))
return IRQ_NONE; // 不是本设备的中断
// 处理中断
writel(status, dev->base + INT_CLEAR); // 清除中断标志
// ... 处理数据 ...
return IRQ_HANDLED;
}
// 注册
request_irq(irq, my_shared_handler, IRQF_SHARED, "my_device", dev);
调试技巧: 通过
/proc/interrupts 可以查看每个IRQ的触发次数和注册的处理函数。如果某个共享中断的处理函数频繁返回 IRQ_NONE,说明该设备可能误触发或硬件存在异常。