2. Linux 中断子系统架构:中断控制器(GIC)、中断描述符、中断处理流程概述
中断子系统是 Linux 内核响应外部异步事件的基石。在 ARM 多核处理器平台上,中断控制器(Generic Interrupt Controller, GIC)负责收集、优先级排序和分发中断信号;内核通过中断描述符(irq_desc)将硬件中断号映射到软件处理逻辑;整个处理流程分为上半部(硬中断)和下半部(软中断/tasklet)。本章从架构层面拆解这三个核心要素。
核心要点:中断子系统 = GIC(硬件路由) + irq_desc(软件抽象) + 处理流程(上下半部分离)。
2.1 中断控制器(GIC)
GIC 是 ARM 架构的标准中断控制器,负责接收外设中断信号,按优先级排序后分发到指定的 CPU 核心。Linux 内核通过 drivers/irqchip/ 下的驱动对 GIC 进行初始化与配置。
2.1.1 GIC 版本与特性对比
| 特性 | GICv2 | GICv3 | GICv4 |
|---|---|---|---|
| CPU 接口 | 内存映射寄存器(GICC) | 系统寄存器(ICC_*) | 系统寄存器 + 虚拟化扩展 |
| 中断号范围 | 0~1019 | 0~8191 | 0~8191 |
| 多核分发 | SPI 支持亲和性 | 支持 SPI + LPI 亲和性 | 支持直接注入虚拟机 |
| LPI(消息中断) | 不支持 | 支持(ITS) | 支持(ITS + vLPI) |
| 虚拟化支持 | 基本(vgic) | 增强(vgic-v3) | 直接 VM 中断注入 |
工程提示:在嵌入式 Linux 开发中,GICv2 仍广泛用于 Cortex-A7/A9 平台;GICv3 是 ARMv8 的标配;GICv4 主要面向服务器和虚拟化场景。驱动开发者通常不需要直接操作 GIC 寄存器,而是通过 irqchip 框架注册回调。
2.1.2 GIC 中断类型
- SGI(Software Generated Interrupt):软件触发的中断,用于核间通信(IPI),中断号 0~15。
- PPI(Private Peripheral Interrupt):私有外设中断,每个 CPU 核心独享,中断号 16~31。
- SPI(Shared Peripheral Interrupt):共享外设中断,可路由到任意 CPU 核心,中断号 32~1019(GICv2)。
- LPI(Locality-specific Peripheral Interrupt):基于消息的中断,通过 ITS(Interrupt Translation Service)路由,中断号 8192 起。
避坑指南:在设备树中配置中断时,注意
interrupt-cells 属性:GICv2 通常为 3(中断类型、中断号、触发方式),GICv3 为 3 或 4。类型标志 0 表示 SPI,1 表示 PPI。
2.2 中断描述符(irq_desc)
内核使用 struct irq_desc 描述每个中断源。该结构体是中断子系统的核心抽象,连接硬件中断号与软件处理函数。
2.2.1 irq_desc 关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
irq_data |
struct irq_data |
硬件相关数据(中断号、chip、chip_data) |
action |
struct irqaction * |
中断处理函数链表(共享中断时多个 handler) |
status_use_accessors |
unsigned int |
中断状态标志(IRQ_DISABLED、IRQ_INPROGRESS 等) |
depth |
unsigned int |
中断禁用深度计数 |
percpu_enabled |
cpumask_var_t |
Per-CPU 中断使能掩码 |
pending_mask |
cpumask_var_t |
待处理 CPU 掩码(用于亲和性) |
kstat_irqs |
struct irqstat * |
中断统计信息 |
2.2.2 中断号映射流程
- 硬件中断号(hwirq):由 GIC 或设备树指定,如 SPI 32 对应 hwirq 32。
- Linux 中断号(irq):通过
irq_domain将 hwirq 映射到全局 irq 号。 - irq_desc 查找:内核通过
irq_to_desc(irq)获取对应的 irq_desc。 - action 链表遍历:中断触发时,遍历
irq_desc->action链表,执行注册的 handler。
关键函数:
request_irq(irq, handler, flags, name, dev) 内部会分配 irqaction,挂入 irq_desc->action 链表,并调用 irq_chip->irq_startup 使能中断。
2.3 中断处理流程概述
Linux 中断处理分为上半部(硬中断上下文)和下半部(软中断上下文)。上半部要求快速完成,下半部处理耗时任务。
2.3.1 完整处理流程
流程速记:硬件触发 → GIC 分发 → CPU 异常向量 → 内核 entry → 通用中断处理 → 调用 handler → 标记软中断 → 退出异常 → 执行软中断/tasklet。
- 硬件阶段:外设拉高中断线,GIC 仲裁后向指定 CPU 发送 IRQ 信号。
- 异常向量:CPU 跳转到
vectors表中的 IRQ 入口(如el1_irq)。 - 内核 entry:保存上下文,调用
handle_arch_irq(GIC 注册的中断处理入口)。 - GIC 处理:
gic_handle_irq读取 GICC_IAR 获取中断号,调用handle_domain_irq。 - 通用中断层:
__handle_domain_irq查找 irq_desc,调用irq_desc->handle_irq(如handle_fasteoi_irq)。 - 执行 handler:遍历 action 链表,执行驱动注册的
irq_handler_t。 - 标记软中断:handler 返回
IRQ_WAKE_THREAD或通过tasklet_schedule触发下半部。 - 退出异常:恢复上下文,返回被中断的进程。
- 下半部执行:在
do_softirq或ksoftirqd中执行 tasklet/softirq。
2.3.2 上下半部分工对比
| 特性 | 上半部(硬中断) | 下半部(软中断/tasklet) |
|---|---|---|
| 执行上下文 | 中断上下文(不可睡眠) | 软中断上下文(不可睡眠) |
| 中断屏蔽 | 本地 CPU 中断关闭 | 不屏蔽硬件中断 |
| 执行时间 | 极短(μs 级) | 可稍长(ms 级) |
| 典型操作 | 清除中断标志、拷贝数据、唤醒 | 协议处理、数据解析、唤醒等待队列 |
| 调度方式 | 硬件触发 | 软件触发(tasklet_schedule) |
| 并发性 | 串行(同一中断号) | 可并行(不同 CPU) |
2.4 中断子系统架构总览(SVG)
下图展示了从外设触发中断到下半部执行的完整架构,包含 GIC、irq_desc、上下半部处理路径。
2.5 关键数据结构关系(SVG)
中断子系统的核心数据结构包括 irq_desc、irq_data、irq_chip、irqaction 和 irq_domain。下图展示了它们之间的引用关系。
2.6 中断处理流程代码示例
以下代码展示了一个典型的中断驱动注册流程,包含上半部 handler 和 tasklet 下半部。
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#define GPIO_IRQ_NUM 32 /* 假设 SPI 32 号中断 */
static struct tasklet_struct my_tasklet;
/* 下半部 tasklet 处理函数 */
static void my_tasklet_handler(unsigned long data)
{
printk(KERN_INFO "tasklet: 处理中断下半部, data=%lu\n", data);
/* 执行耗时操作:数据解析、协议处理等 */
}
/* 上半部中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "上半部: 中断 %d 触发\n", irq);
/* 清除硬件中断标志(由芯片相关操作完成) */
/* 调度 tasklet 执行下半部 */
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
static int __init my_driver_init(void)
{
int ret;
/* 初始化 tasklet */
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
/* 注册中断 */
ret = request_irq(GPIO_IRQ_NUM, my_irq_handler,
IRQF_TRIGGER_RISING, "my_device", NULL);
if (ret) {
printk(KERN_ERR "request_irq failed: %d\n", ret);
return ret;
}
printk(KERN_INFO "驱动加载成功,中断 %d 已注册\n", GPIO_IRQ_NUM);
return 0;
}
static void __exit my_driver_exit(void)
{
/* 注销中断 */
free_irq(GPIO_IRQ_NUM, NULL);
/* 销毁 tasklet */
tasklet_kill(&my_tasklet);
printk(KERN_INFO "驱动卸载完成\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux 驱动专家");
MODULE_DESCRIPTION("中断处理与 tasklet 示例");
代码要点:
request_irq注册上半部 handler,在中断上下文中执行。tasklet_schedule在 handler 中调用,触发下半部。tasklet_init在模块初始化时创建 tasklet,tasklet_kill在卸载时销毁。- 上半部应尽量短,只做必要操作(如清除标志、调度下半部)。
2.7 本章小结
| 知识点 | 核心内容 | 工程应用 |
|---|---|---|
| GIC 中断控制器 | SGI/PPI/SPI/LPI 类型,GICv2/v3/v4 差异 | 设备树配置中断类型与触发方式 |
| irq_desc 描述符 | 连接硬件中断号与软件 handler | 通过 irq_to_desc 调试中断状态 |
| 上下半部分离 | 上半部快速响应,下半部延迟处理 | tasklet 用于协议解析、数据拷贝 |
| 处理流程 | 硬件触发 → GIC → CPU → handler → tasklet | 遵循流程编写高效中断驱动 |