4. GICv3中断控制器配置:GICv3与GICv2的区别、ITS节点配置、LPI的使能

好,咱们今天聊聊GICv3。说实话,这玩意儿刚出来的时候,我也觉得头大。GICv2用了那么多年,大家都习惯了,突然来个v3,改动还不小。但等你真正用上,就会发现——嗯,真香。

4.1 GICv3与GICv2的核心区别

先说说最直观的区别。GICv2时代,CPU接口是直接挂在芯片内部的,每个核有自己的GIC CPU接口。但到了GICv3,架构变了——它把CPU接口和分发器彻底分离了。

我个人习惯用一张表来对比,这样最清楚:

特性 GICv2 GICv3
CPU接口 内存映射,每个核独立 系统寄存器方式(ICC_*_ELx)
中断号范围 最多1020个SPI 支持16K SPI,支持LPI(可达16M)
亲和性路由 不支持 支持多级亲和性(Affinity路由)
LPI支持 不支持 通过ITS支持消息中断
虚拟化支持 基础支持 硬件加速虚拟化(LPI直接注入VM)

你想想看,GICv2的中断路由是固定的——每个中断绑定到特定CPU。但GICv3不一样,它引入了亲和性路由的概念。说白了,就是中断可以灵活地发给某个集群、某个核,甚至可以在运行时动态调整。

我在项目中遇到过一个问题:某款服务器芯片有128个核,用GICv2的话,中断路由表会爆炸。换成GICv3后,用亲和性路由轻松搞定。这就是为什么大型ARM服务器都强制要求GICv3。

4.2 ITS节点配置

ITS,全称Interrupt Translation Service,翻译过来就是中断翻译服务。这玩意儿是GICv3的灵魂。

为什么要ITS?因为LPI(Local Peripheral Interrupt)不是直接发到GIC的,而是通过写内存的方式触发。设备往内存里写一个数据包,ITS负责把这个数据包翻译成中断信号,再路由到目标CPU。

来看一个典型的ITS设备树节点:

its: interrupt-controller@2c010000 {
    compatible = "arm,gic-v3-its";
    reg = <0x0 0x2c010000 0x0 0x20000>;
    msi-controller;
    #msi-cells = <1>;
    soc: soc {
        #address-cells = <2>;
        #size-cells = <2>;
        ...
    };
};

这里有几个关键点,我重点说一下:

  • compatible:必须是"arm,gic-v3-its",别写错了。我见过有人写成"arm,gic-its",结果内核根本不认。
  • reg:ITS的寄存器基地址。注意,这个地址要和GIC分发器的地址空间分开。
  • msi-controller:这个属性告诉内核,这个节点是一个MSI控制器。没有它,PCIe设备发不了MSI中断。
  • #msi-cells:通常设为1,表示每个MSI需要一个32位的标识符。
注意:ITS节点必须作为GIC节点的子节点存在。我曾经犯过一个错误,把ITS节点放在了根节点下,结果中断根本路由不过去。内核报错"ITS not attached to GIC",查了半天才发现是层级关系搞错了。

4.3 LPI的使能

LPI,全称Local Peripheral Interrupt。它和传统的SPI、PPI不一样——LPI是基于消息的中断。

怎么理解呢?传统中断是设备拉一根物理信号线到GIC,GIC检测到电平变化就触发中断。但LPI不是这样——设备往内存里写一个特定的数据包,ITS收到后解析这个包,然后生成中断。

使能LPI需要做几件事:

  1. 配置GICR寄存器:每个CPU核都有一个GIC Redistributor(GICR),LPI的使能位就在GICR中。
  2. 设置LPI配置表:LPI的优先级、使能状态等信息存储在内存中的配置表里。
  3. 初始化ITS:ITS需要先完成初始化,才能接收和翻译LPI消息。

设备树中使能LPI的典型配置:

gic: interrupt-controller@2c000000 {
    compatible = "arm,gic-v3";
    reg = <0x0 0x2c000000 0x0 0x10000>,  // GICD
          <0x0 0x2c100000 0x0 0x200000>, // GICR
          <0x0 0x2c300000 0x0 0x100000>; // GICR(第二个CPU的)
    interrupt-controller;
    #interrupt-cells = <4>;
    #address-cells = <2>;
    #size-cells = <2>;
    
    its: interrupt-controller@2c010000 {
        compatible = "arm,gic-v3-its";
        reg = <0x0 0x2c010000 0x0 0x20000>;
        msi-controller;
        #msi-cells = <1>;
    };
};
小技巧:GICR的地址范围要覆盖所有CPU核。比如你有4个核,每个核的GICR占用0x20000字节,那GICR的总大小就是0x80000。我习惯在reg属性里把每个核的GICR地址都列出来,这样调试时一眼就能看出哪个核的GICR配置有问题。

嗯,这里要注意一点:LPI的使能不是一次性完成的。你需要先确保ITS正常工作,然后才能使能LPI。顺序搞反了,LPI会一直pending,但永远不会被处理。

我曾经在调试NVMe SSD驱动时遇到过这个问题。设备发了MSI中断,但CPU就是收不到。查了两天,最后发现是ITS还没初始化完成,我就先把LPI使能了。结果LPI消息到了ITS,ITS还没准备好,直接丢弃了。从那以后,我每次写LPI使能代码,都会先检查ITS的状态寄存器。

4.4 避坑指南

最后,分享几个我踩过的坑:

  • GICv3的#interrupt-cells是4,不是3。GICv2用3个cell就够了,但GICv3需要4个——分别是中断类型、中断号、触发类型、亲和性信息。我见过有人直接复制GICv2的配置,结果中断路由全乱了。
  • ITS的reg地址不能和GICD重叠。虽然它们都是GIC的一部分,但地址空间是独立的。重叠的话,内核会报地址冲突。
  • LPI配置表必须放在非cacheable内存。因为ITS是通过DMA方式访问配置表的,cache一致性处理不好会出问题。我习惯在设备树里用"no-map"属性保留一段内存给LPI配置表用。

好了,GICv3的基础配置就讲到这里。下一节我们会深入ITS的翻译机制,看看它到底是怎么把设备消息变成中断信号的。到时候我会带一个实际案例,保证让你彻底搞懂。