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 的寄存器布局完全不同,不能混用。

特性GICv2GICv3GICv4
最大 CPU 核数8无限制(通常 256)同 v3
中断号范围0-10190-65535同 v3
亲和性表示CPU 接口号 (0-7)MPIDR 寄存器值同 v3
虚拟化支持基础增强LPI 直通
寄存器访问内存映射 (MMIO)MMIO + 系统寄存器同 v3

14.2 GICv2 配置与中断路由

GICv2 的配置相对直观。它由两个主要部分组成:Distributor(分发器)和 CPU Interface(CPU 接口)。Distributor 负责全局中断管理,CPU Interface 负责给每个核分发中断。

配置一个 SPI(共享外设中断)的基本步骤,我总结为三步:

  1. 配置 Distributor:设置中断优先级、目标 CPU、触发方式(边沿/电平)。
  2. 使能中断:在 Distributor 层使能该中断。
  3. 使能 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 位寄存器,包含 Aff0Aff1Aff2Aff3 四个字段,可以表示复杂的层级结构。

比如一个 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 提供了两种策略:

  1. 静态绑定:在系统初始化时,把中断固定到某个核。适合确定性要求高的场景。
  2. 动态迁移:运行时根据 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_VPENDBASERGICR_VPROPBASER 寄存器。这部分内容比较深,我建议你先掌握 GICv3 的配置,再逐步深入。

好了,这一章的内容就到这里。中断控制器配置是 SMP 系统的基石,搞懂了 GIC 的亲和性设置,你就能灵活控制中断的流向,让每个 CPU 核都物尽其用。下一章我们聊聊核间通信(IPI),那是多核协作的另一个关键点。