1. 中断基础概念:什么是中断、中断向量表、中断控制器(GIC)介绍

大家好,我是老李。做嵌入式Linux开发这么多年,我敢说中断处理是绕不过去的坎。很多刚入行的朋友觉得中断很神秘,其实说白了,它就是CPU处理突发事件的一种机制。今天咱们就从最基础的概念开始,把中断这层窗户纸捅破。

1.1 什么是中断?

想象一下,你正在专心写代码,突然有人拍你肩膀说“快递到了”。你会怎么做?先停下手头的工作,去拿快递,然后回来继续写代码。中断就是这个道理——CPU正在执行主程序,突然收到一个信号,就暂停当前任务,去处理这个信号,处理完再回来接着干。

中断的核心价值在于:它让CPU不用一直轮询等待某个事件发生。我早期做单片机项目时,就吃过轮询的亏——CPU一直在那里傻等按键按下,啥正事都干不了。后来换成中断方式,CPU该干嘛干嘛,按键按下时自动跳过去处理,效率提升了一大截。

中断的本质:一种硬件机制,允许外设或软件通过信号打断CPU当前执行流,跳转到预定义的 handler 去处理事件。

中断分两种:

  • 硬件中断:由外部设备触发,比如网卡收到数据包、按键按下、定时器溢出。这是咱们做驱动开发打交道最多的。
  • 软件中断:由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改写了,内核启动后没重新设置——这种坑,踩过一次就记住了。

我的经验:调试中断问题时,第一步永远是确认中断向量表是否正确安装。用/proc/interrupts可以查看系统当前的中断注册情况,这是最直接的诊断手段。

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的中断处理流程大致是这样的:

  1. 外设拉高中断请求线
  2. Distributor检测到中断,根据优先级和路由配置,决定发给哪个CPU
  3. CPU Interface收到中断,向CPU发送IRQ信号
  4. CPU保存现场,跳转到中断向量表执行handler
  5. handler处理完,写EOI(End of Interrupt)寄存器通知GIC
  6. GIC清除该中断状态,继续等待下一个中断

下面这张图是我自己画的GIC工作流程,你看一眼就明白了:

GIC中断处理流程 外设(Device) Distributor 使能/优先级/路由 CPU Interface 通知CPU CPU Core 中断向量表 中断Handler 写EOI寄存器 流程:外设→Distributor→CPU Interface→CPU→查向量表→执行Handler→写EOI→结束
避坑指南:我曾经在调试一个双核系统时,发现中断偶尔丢失。查了两天才发现,GIC的Distributor配置了中断只发给CPU0,但CPU0当时正在关中断执行临界区代码,中断来了没人接。解决方案是把中断配置成“任意CPU可处理”,或者用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上。这两个命令是中断调试的“瑞士军刀”。