1. 中断基础概念:什么是中断、中断向量表、中断控制器(GIC)介绍
大家好,我是老李。做嵌入式Linux开发这么多年,我敢说中断处理是绕不过去的坎。很多刚入行的朋友觉得中断很神秘,其实说白了,它就是CPU处理突发事件的一种机制。今天咱们就从最基础的概念开始,把中断这层窗户纸捅破。
1.1 什么是中断?
想象一下,你正在专心写代码,突然有人拍你肩膀说“快递到了”。你会怎么做?先停下手头的工作,去拿快递,然后回来继续写代码。中断就是这个道理——CPU正在执行主程序,突然收到一个信号,就暂停当前任务,去处理这个信号,处理完再回来接着干。
中断的核心价值在于:它让CPU不用一直轮询等待某个事件发生。我早期做单片机项目时,就吃过轮询的亏——CPU一直在那里傻等按键按下,啥正事都干不了。后来换成中断方式,CPU该干嘛干嘛,按键按下时自动跳过去处理,效率提升了一大截。
中断分两种:
- 硬件中断:由外部设备触发,比如网卡收到数据包、按键按下、定时器溢出。这是咱们做驱动开发打交道最多的。
- 软件中断:由CPU执行指令触发,比如系统调用(ARM的SVC指令)、异常(缺页、除零)。Linux内核的system_call就是靠软中断实现的。
嗯,这里要注意一个容易混淆的概念:异常(Exception)。很多人把异常和中断混为一谈。我的理解是:中断是“外部事件”,异常是“内部错误”。比如你写了个野指针访问,触发了缺页异常,这可不是外设干的,是CPU自己发现不对劲了。
1.2 中断向量表
中断来了,CPU怎么知道该跳到哪里去处理?这就得靠中断向量表了。
中断向量表,说白了就是一个地址表。每个中断源都有一个对应的表项,里面存放着该中断的处理函数地址。CPU收到中断后,根据中断号去查这个表,找到对应的handler,直接跳过去执行。
我当年在ARM9平台上做Bootloader时,就亲手写过中断向量表。ARM的向量表固定在0x00000000地址,前8个表项分别是:
| 偏移地址 | 异常类型 | 优先级 | 说明 |
|---|---|---|---|
| 0x00 | 复位 | 1(最高) | 系统上电或复位后执行 |
| 0x04 | 未定义指令 | 6 | 遇到无法识别的指令 |
| 0x08 | 软件中断(SVC) | 6 | 执行SVC指令触发 |
| 0x0C | 预取中止 | 5 | 指令预取失败 |
| 0x10 | 数据中止 | 2 | 数据访问失败 |
| 0x14 | 保留 | - | 未使用 |
| 0x18 | IRQ | 4 | 普通中断请求 |
| 0x1C | FIQ | 3 | 快速中断请求 |
在Linux内核里,中断向量表的管理更复杂。x86架构下,内核用idt_table来管理;ARM架构下,内核启动时会设置VBAR(Vector Base Address Register)指向自己的向量表。我记得有一次调试一个中断没响应的问题,查了半天发现是VBAR被Bootloader改写了,内核启动后没重新设置——这种坑,踩过一次就记住了。
1.3 中断控制器(GIC)介绍
现代SoC里外设太多了,几十上百个中断源,CPU不可能每个都直接管。这时候就需要一个中断控制器来统一管理。ARM架构下,这个角色就是GIC(Generic Interrupt Controller)。
GIC从v1发展到v3/v4,咱们做嵌入式Linux,最常见的是GICv2和GICv3。GICv2在Cortex-A7/A9上很普遍,GICv3在Cortex-A53/A72等64位处理器上开始普及。
GIC的核心架构分为两大部分:
- Distributor(分发器):负责中断的使能、优先级配置、中断类型设置。它接收所有外设的中断信号,然后决定发给哪个CPU。
- CPU Interface(CPU接口):每个CPU核都有一个,负责和CPU交互。它从Distributor拿到中断,然后通知CPU去处理。
GIC支持三种中断类型:
| 类型 | ID范围 | 特点 | 典型用途 |
|---|---|---|---|
| SPI(共享外设中断) | 32~1019 | 可以路由到任意一个CPU | 网卡、USB、UART等外设 |
| PPI(私有外设中断) | 16~31 | 每个CPU独有,不共享 | 本地定时器、性能计数器 |
| SGI(软件生成中断) | 0~15 | CPU通过写寄存器触发 | 核间通信(IPI) |
你想想看,为什么要有SPI和PPI的区分?我在项目中遇到过这样一个场景:双核处理器,网卡中断来了,两个CPU都能处理,但最好让其中一个专门处理,避免争抢。SPI就可以配置成只发给CPU0。而每个CPU的本地定时器,那是私有的,用PPI最合适。
GIC的中断处理流程大致是这样的:
- 外设拉高中断请求线
- Distributor检测到中断,根据优先级和路由配置,决定发给哪个CPU
- CPU Interface收到中断,向CPU发送IRQ信号
- CPU保存现场,跳转到中断向量表执行handler
- handler处理完,写EOI(End of Interrupt)寄存器通知GIC
- GIC清除该中断状态,继续等待下一个中断
下面这张图是我自己画的GIC工作流程,你看一眼就明白了:
1.4 Linux中的中断号映射
搞清楚了GIC的硬件中断号(HW IRQ),还得理解Linux内核里的软件中断号(IRQ number)。这两者不是一回事。
GIC给每个中断分配了一个硬件ID,比如SPI中断从32开始。但Linux内核为了统一管理,会把这个硬件ID映射到一个软件IRQ号上。这个映射关系由irq_domain来管理。
举个例子:GICv2上,SPI中断的硬件ID是32,Linux内核可能把它映射成IRQ号16。你在驱动里request_irq(16, handler, ...),实际上注册的是GIC的32号中断。
查看映射关系的方法:
# cat /proc/interrupts
CPU0 CPU1
16: 12345 67890 GICv2 32 eth0
17: 10 5 GICv2 33 uart0
第一列是Linux IRQ号,最后一列是硬件中断源。中间那个数字(32、33)就是GIC的硬件中断ID。
cat /proc/interrupts确认中断号,再用cat /proc/irq/16/smp_affinity看中断绑在哪个CPU上。这两个命令是中断调试的“瑞士军刀”。