中断机制深度解析:APIC、GIC、中断向量与处理流程

大家好,我是你们的老朋友。今天我们聊聊中断机制。这东西,说白了就是CPU怎么响应外部事件的“通讯协议”。我在调优DMA中断亲和性的时候,发现好多问题根子就在这层没搞透。你想想看,连中断是怎么从设备走到CPU核心的都不知道,谈何优化它被哪个核处理呢?

我个人习惯,把中断机制拆成三块来看:谁管的(中断控制器)、管什么的(中断向量)、怎么管的(处理流程)。我们一个一个过。

3.1 中断控制器:APIC 与 GIC 的差异

中断控制器,就是CPU外围的“总调度员”。x86体系用APIC,ARM体系用GIC。它们功能类似,但设计哲学和配置方式差别挺大。我当年从x86转去调ARM平台时,在GIC路由配置上踩过坑,嗯,后面细说。

3.1.1 x86 的 APIC(高级可编程中断控制器)

APIC分两层:一个全局的I/O APIC(通常集成在南桥或CPU内部),和每个CPU核心私有的Local APIC。设备的中断信号先到达I/O APIC,I/O APIC根据路由表(Redirection Table)把中断发往某个或某几个Local APIC,最终由具体的CPU核心响应。

  • I/O APIC:接收外设中断,可编程路由到任意一个CPU核心。
  • Local APIC:每个核一个,管理本地中断(如时钟中断、IPI核间中断),并接收来自I/O APIC的中断消息。
我个人习惯:在BIOS中或通过Linux的acpi_irq_model,我会确认APIC模式是否启用(而不是老旧的PIC/8259模式)。PIC模式下根本没法做中断亲和性绑定,所有中断都哄抢CPU 0,那性能画面太美我不敢看。

3.1.2 ARM 的 GIC(通用中断控制器)

GIC版本很多,从GIC-400到GIC-500、GIC-600。核心架构分为三部分:Distributor(分发器)、Redistributor(重分发器)、CPU Interface(CPU接口)。Distributor负责全局中断配置,每个CPU核心有自己的Redistributor和CPU Interface。

  • Distributor:配置中断优先级、触发方式(边沿/电平)、目标CPU列表。
  • Redistributor:处理每个核私有的PPI(私有外设中断)和SGI(软件触发中断)。
  • CPU Interface:向核心发送中断信号,控制中断屏蔽和应答。

GIC的亲和性配置更灵活,因为每个中断的affinity可以在运行时动态调整到任意核心组合。我在调一个DPDK网卡时,就用GIC的亲和性子系统把网卡中断固定到了前两个物理核,效果立竿见影。

特性 APIC (x86) GIC (ARM)
路由颗粒度 基于Redirection Table条目 基于每个中断ID的亲和性子域
私有中断 Local APIC处理时钟、IPI Redistributor处理PPI、SGI
亲和性配置 通过IOREDTBL寄存器的destination字段 通过GICD_IROUTER寄存器
中断优先级 支持,但现代OS用得不多 更灵活的优先级分组模型

3.2 中断向量:中断号与处理函数映射

每个硬件中断都有一个唯一的编号,就是我们常说的中断向量(Interrupt Vector)。x86上中断向量范围0~255,ARM GIC中是每个SPI/PPI/SGI有唯一的Interrupt ID。内核拿到这个向量号,去查一张全局表——中断描述符表(IDT on x86),找到对应的处理函数(ISR)。

嗯,这里要注意:中断向量不是随便分的。Linux内核把x86的中断向量划成了几段:

  • 0x00–0x1F:CPU异常和陷阱(如缺页、除零)。
  • 0x20–0x2F:IRQ 0–15(历史上保留给旧PIC)。
  • 0x30–0xFF:动态分配,用于MSI-X、IOAPIC中断。

ARM GIC的Interrupt ID则从0开始:ID0-ID15是SGI;ID16-ID31是PPI;ID32往后是共享的SPI。我在项目中遇到过一个问题:某个外设注册的中断号是GIC SPI 168,结果因为设备树写错了,导致中断路由到一个离线核心上,整个系统响应变得奇慢。

避坑指南:我曾经在调试一个NVMe SSD时,发现中断响应延迟忽高忽低。查到最后,是IRQ向量号在系统启动时被重复分配给两个不同的MSI-X队列。因为我们在BIOS层面没有正确预留向量空间。记住:中断向量是有限资源,尤其当你有大量MSI-X设备时。

3.3 中断处理流程:从硬件到软件

有了前面的控制器和向量,我们来看中断从触发到完成的全流程。我把它归纳为6步,画了张SVG图,你对照着看会更清楚。

中断处理全流程 ① 设备触发 外设产生中断请求 ② 中断控制器路由 APIC/GIC 查找路由表 ③ CPU 仲裁 目标核接受中断信号 ④ 向量查表 IDT/GIC 找 ISR 入口 ⑤ ISR (上半部) 最小化处理,清中断 ⑥ 下半部处理 SoftIRQ/Tasklet 关键说明: • 中断不能被嵌套抢占同类中断(硬件自动屏蔽同级)。 • ISR 必须快速完成,否则会阻塞其他中断。我习惯在 ISR 中只做 ACK 和调度下半部。 • 下半部是可延迟执行的,可以开中断,允许被其他中断打断。

结合上面的图,我把第⑤和第⑥步再展开说说:

  1. 上半部(ISR):CPU收到中断后,自动关中断(或硬件屏蔽该中断),执行注册的ISR。ISR里通常干三件事:读中断状态寄存器确认原因;写中断结束寄存器(EOI)通知控制器;调度下半部(如raise_softirq)。
  2. 下半部(SoftIRQ/Tasklet/Workqueue):核心再次开中断后,在合适时机(如从ISR返回前、调度点)执行。下半部可以长时间运行,也允许其他中断发生。
  3. 中断优先级与抢占:高优先级中断可以打断低优先级中断的上半部。我在一次线上问题时发现,某块网卡的RX队列中断优先级设得比时钟中断还高,结果时钟漂移严重,系统挂死。

实战要点:优化DMA中断亲和性,本质就是优化步骤②(中断控制器路由)和步骤③(CPU仲裁)的分配策略。我建议你在/proc/interrupts里先看清每个IRQ当前被哪个核心处理了,然后用echo配合smp_affinity手动改,这是最快验证中断分布是否合理的方法。

好,中断机制这块东西比较多,但你只要记住:控制器管路由,向量管映射,流程管先后顺序。做到这三点,后面谈亲和性优化和动态均衡时,就不会犯低级错误了。