14、中断控制器:GICv2/v3/v4配置、中断路由、亲和性设置
好,我们进入第十四章。中断控制器,这是多核系统的“神经中枢”。你想想看,CPU 核再多,如果中断来了不知道该发给谁,或者发错了核,那系统就乱套了。在 ARM 多核世界里,GIC(Generic Interrupt Controller)就是干这个活的。
我个人习惯把 GIC 理解成一个“智能路由器”。它不光要接收外设的中断信号,还要根据你的配置,把中断精准地路由到指定的 CPU 核上。今天我们就来聊聊 GICv2、v3、v4 的配置,以及中断路由和亲和性设置这些硬核话题。
核心要点:中断亲和性(Affinity)决定了哪个 CPU 核来处理这个中断。配置得当,能大幅提升缓存命中率和系统性能;配置不当,就是“中断风暴”,CPU 核之间互相抢活干。
14.1 GIC 版本演进:从 v2 到 v4
先简单过一下版本。GICv2 是经典款,在 Cortex-A7/A9/A15 时代很常见。它支持最多 8 个 CPU 核,中断号范围 0-1019。GICv3 呢,是为 64 位 ARMv8 架构设计的,支持超过 16 个核,中断号扩展到 16K+。GICv4 在 v3 基础上加了虚拟化扩展,对虚拟机中断直通做了优化。
我在项目中遇到过最头疼的,就是老平台升级时,代码里还硬编码着 GICv2 的寄存器地址。结果一跑就挂。嗯,这里要注意:GICv2 和 GICv3 的寄存器布局完全不同,不能混用。
| 特性 | GICv2 | GICv3 | GICv4 |
|---|---|---|---|
| 最大 CPU 核数 | 8 | 无限制(通常 256) | 同 v3 |
| 中断号范围 | 0-1019 | 0-65535 | 同 v3 |
| 亲和性表示 | CPU 接口号 (0-7) | MPIDR 寄存器值 | 同 v3 |
| 虚拟化支持 | 基础 | 增强 | LPI 直通 |
| 寄存器访问 | 内存映射 (MMIO) | MMIO + 系统寄存器 | 同 v3 |
14.2 GICv2 配置与中断路由
GICv2 的配置相对直观。它由两个主要部分组成:Distributor(分发器)和 CPU Interface(CPU 接口)。Distributor 负责全局中断管理,CPU Interface 负责给每个核分发中断。
配置一个 SPI(共享外设中断)的基本步骤,我总结为三步:
- 配置 Distributor:设置中断优先级、目标 CPU、触发方式(边沿/电平)。
- 使能中断:在 Distributor 层使能该中断。
- 使能 CPU 接口:在每个核的 CPU Interface 中使能中断接收。
关键寄存器是 GICD_ITARGETSRn。每个 SPI 中断对应一个寄存器,里面 8 位分别代表 8 个 CPU 核。比如你想让中断 32 只发给 CPU0,就写 0x01;发给 CPU0 和 CPU1,就写 0x03。
// GICv2 配置示例:将中断 32 路由到 CPU0
#define GICD_BASE 0x08000000
#define GICD_ITARGETSR (GICD_BASE + 0x800)
// 中断 32 对应的 ITARGETSR 偏移:32 / 4 * 4 = 32
uint32_t *target_reg = (uint32_t *)(GICD_ITARGETSR + 32);
*target_reg = 0x01; // 只发给 CPU0
// 使能中断
uint32_t *enable_reg = (uint32_t *)(GICD_BASE + 0x100 + (32 / 32) * 4);
*enable_reg = (1 << (32 % 32));
// 在 CPU0 上使能 CPU 接口
#define GICC_BASE 0x08010000
uint32_t *icc_reg = (uint32_t *)(GICC_BASE + 0x00);
*icc_reg = 0x01; // 使能组 0 中断
避坑指南:我曾经在配置 GICv2 时,忘了每个 ITARGETSR 寄存器控制 4 个连续的中断。中断 32-35 共用一个寄存器。如果你只改低 8 位,高 24 位保持默认值 0,那中断 33-35 就全发给 CPU0 了。正确做法是读-改-写。
14.3 GICv3/v4 配置:亲和性新玩法
到了 GICv3,事情变得更有意思了。亲和性不再用简单的 8 位掩码,而是用 MPIDR(Multiprocessor Affinity Register)值来表示。MPIDR 是一个 64 位寄存器,包含 Aff0、Aff1、Aff2、Aff3 四个字段,可以表示复杂的层级结构。
比如一个 4 核 CPU,MPIDR 值可能是:
- CPU0:
0x00000000 - CPU1:
0x00000001 - CPU2:
0x00000100(Aff1=1, Aff0=0) - CPU3:
0x00000101(Aff1=1, Aff0=1)
配置中断路由时,GICv3 使用 GICD_IROUTERn 寄存器。每个 SPI 中断都有一个独立的 64 位路由寄存器,你可以直接写入目标 CPU 的 MPIDR 值。
// GICv3 配置示例:将中断 64 路由到 CPU1 (MPIDR=0x01)
#define GICD_BASE 0x2F000000
#define GICD_IROUTER (GICD_BASE + 0x6000)
uint64_t *router_reg = (uint64_t *)(GICD_IROUTER + 64 * 8);
*router_reg = 0x01; // 路由到 MPIDR=0x01 的核
// 或者路由到所有核(中断广播)
*router_reg = 0xFFFFFFFF; // 特殊值,表示所有核
注意:GICv3 中,如果 GICD_IROUTERn 的 bit[31] 设置为 1,表示中断路由到所有核。但这是“中断风暴”的常见来源。我建议只在非常特定的场景(比如全局时钟同步)下使用广播模式。
14.4 中断亲和性设置实战
在 RT-Thread SMP 中,中断亲和性设置通常通过 rt_hw_interrupt_set_affinity() 函数完成。这个函数会调用底层 GIC 驱动,修改对应的路由寄存器。
我一般建议遵循以下原则:
- 核 0 专用中断:定时器、IPI(核间中断)等系统级中断,固定路由到核 0。
- 外设中断:根据负载均衡策略,动态分配到空闲核。
- 高优先级中断:固定到某个核,避免迁移带来的延迟。
举个例子,在 RT-Thread 中设置网卡中断亲和性:
#include <rtthread.h>
#include <gic.h>
void set_eth_irq_affinity(int cpu_id)
{
// 假设网卡中断号为 67
int irq = 67;
// 获取目标 CPU 的 MPIDR
uint64_t mpidr = gic_cpu_mpidr(cpu_id);
// 设置亲和性
gic_set_irq_affinity(irq, mpidr);
rt_kprintf("IRQ %d affinity set to CPU %d (MPIDR=0x%x)\n",
irq, cpu_id, mpidr);
}
个人经验:我曾经在一个 8 核系统上做网络性能优化。默认所有中断都发到核 0,结果核 0 的 CPU 占用率飙到 90%,其他核闲得发慌。后来我把网卡中断分散到核 0-3,每个核处理一个 RX 队列,吞吐量直接翻倍。这就是亲和性的威力。
14.5 中断负载均衡策略
说到负载均衡,RT-Thread SMP 提供了两种策略:
- 静态绑定:在系统初始化时,把中断固定到某个核。适合确定性要求高的场景。
- 动态迁移:运行时根据 CPU 负载,把中断迁移到空闲核。适合通用场景。
动态迁移的实现,说白了就是修改 GIC 的路由寄存器。但要注意,迁移过程中可能会丢失中断。我建议在迁移前先屏蔽中断,迁移完成后再使能。
// 动态迁移中断示例
void migrate_irq(int irq, int new_cpu)
{
// 1. 屏蔽中断
gic_disable_irq(irq);
// 2. 等待正在处理的中断完成
while (gic_is_irq_active(irq));
// 3. 修改路由
gic_set_irq_affinity(irq, gic_cpu_mpidr(new_cpu));
// 4. 重新使能
gic_enable_irq(irq);
}
警告:千万不要在中断处理函数里做迁移!我曾经犯过这个错,结果死锁了。中断处理函数应该尽量短小,迁移操作放到线程上下文去做。
14.6 GICv4 的虚拟化中断
最后简单提一下 GICv4。它引入了 LPI(Locality-specific Peripheral Interrupt)和 vLPI(virtual LPI)。说白了,就是让虚拟机可以直接处理中断,不需要 hypervisor 介入。这对虚拟化场景下的性能提升很大。
配置 vLPI 需要用到 GICR_VPENDBASER 和 GICR_VPROPBASER 寄存器。这部分内容比较深,我建议你先掌握 GICv3 的配置,再逐步深入。
好了,这一章的内容就到这里。中断控制器配置是 SMP 系统的基石,搞懂了 GIC 的亲和性设置,你就能灵活控制中断的流向,让每个 CPU 核都物尽其用。下一章我们聊聊核间通信(IPI),那是多核协作的另一个关键点。