Linux 内核 GICv3+ITS 翻译表完整指南:MSI 中断如何变成 LPI 并路由到目标 CPU
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
设备越加越多,可用的 SPI 中断号却只有 1020 个,绑核还改不动。Linux 内核 GICv3 里的 ITS 翻译表解决的就是这个痛点:把设备发来的 MSI 中断消息现场翻译成 LPI 中断号,再路由到目标 CPU。本文讲清它怎么工作,设备树配置 ITS 怎么写,驱动建表流程长什么样。
一、先看清楚问题:SPI 路由的两个死穴
SPI 这条路走到后期会卡住,卡在两件事上:中断号不够用,和中断绑死了地方动不了。
第一,中断号耗尽。GICv3 的 SPI 编号范围是 32 到 1023,一共 1020 个,全系统共享。今天插一块网卡、明天加两块 NVMe、后天再上 RAID 卡,每块设备想独占一路 SPI 就得占一个号,号池见底只是时间问题。
第二,绑核僵化。SPI 是电平/边沿触发的硬件线路,中断号和产生它的那块外设是物理上焊死的。你想把某个外设的中断从 CPU2 挪到 CPU5 做负载均衡,可以改亲和性;但你没法凭空"多造一个"某外设的 SPI,也没法让同一个外设的不同队列各占一路线——线路就是那么多。
而 PCIe 设备的 MSI 消息天然不同:它走的是 TLP 包,用消息里的地址加数据字段编码"我是谁、哪个事件"。消息数量基本不受硬件线数限制。缺的不是中断,是把消息和中断号对上号的机制。
二、一个类比讲透 ITS:中断号码的转接台
把 ITS 想象成总机转接台:设备打来的每个 MSI 消息都是一次来电,ITS 查表把"哪个设备、哪个事件"翻译成内部工单号(LPI),再按分机规则拨给对应 CPU。
ITS 维护三张表,恰好对应转接台里的三样东西。
Device Table 是登记簿:以 DeviceID 为索引,记下该设备那张翻译表的基地址。来电先报分机号,转接台先翻登记簿。
Interrupt Translation Table(ITT)是分机对照表:以 EventID 为索引,查到对应的 LPI 中断号(INTID)。报出事件,转接台翻出工单号。
Collection Table 是值班表:LPI 该叫醒哪些 CPU 由它决定,这也是运行时改亲和性的落点。
整条链路一句话串起来:外设发 MSI 消息 → ITS 从地址里拆出 DeviceID 和 EventID → Device Table 找到该设备的 ITT → ITT 把 EventID 换成 LPI 号 → Collection 决定发给哪个 CPU → 目标 CPU 执行中断处理程序。
上面这张图把整条链路画全了:上半部分是数据面(消息怎么变中断),下半部分提示这些表其实是内核通过命令队列预建好的。
三、内核视角三步走:探测、建表、注册 MSI domain
驱动干三件事:找到硬件并打开它,把内存里的表建好,然后把自己挂成 MSI domain 供上层申请中断。
第一步探测:irq-gic-v3-its.c 按设备树里compatible = "arm,gic-v3-its"的节点逐个 ITS 初始化。第二步建表:为每个 ITS 分配命令队列(4K 到 64K 的页),再分配 Device Table 和各 Collection;表内容通过 ITS 命令队列写入,命令顺序是 MAPD(绑设备)→ MAPC(绑 collection)→ MAPI(EventID 映射 INTID)→ MOV(设亲和)→ SYNC(刷缓存使其生效)。第三步注册:its_init_domain()把 ITS 注册成 MSI domain,此后 PCIe 设备用标准 MSI 接口申请中断,内核自动翻译成走 ITS 的 LPI。
驱动的关键路径可以精简成这样:
/* 每个 ITS 实例的初始化主干(精简自 its_probe_one) */ page = its_alloc_pages(...); /* 分配命令队列内存,内核往这里写命令 */ its->cmd_base = (void *)page_address(page); err = its_alloc_tables(its); /* 建 Device Table,登记设备入口 */ if (err) goto out_free_cmd; err = its_alloc_collections(its); /* 建 Collection,决定 LPI 发给谁 */ if (err) goto out_free_tables; gits_write_cbaser(baser, its->base + GITS_CBASER); /* 告诉 ITS 命令队列在哪 */ ctlr |= GITS_CTLR_ENABLE; /* 打开 ITS 硬件 */ writel_relaxed(ctlr, its->base + GITS_CTLR); err = its_init_domain(its); /* 注册 MSI domain,设备从此能申请 MSI */这段在做什么:先给"转接台"拨号(命令队列),再摆好三张表的架子,最后挂牌营业。真正的设备绑定和事件映射不是这里硬编码的,而是之后经 MSI domain 回调触发:its_alloc_device登记设备、its_map_irq建 EventID 到 LPI 的映射、its_set_affinity改目标 CPU,全部翻译成命令队列里的 MAPD / MAPI / MOV 命令执行。
四、动手配置:一份能用的 GICv3+ITS 设备树
设备树里 ITS 就是一个带msi-controller标签的节点,写对四个属性就够:compatible、reg、msi-parent、#msi-cells。
/* GIC 分发器 + 重分布器 */ gic: interrupt-controller@2c000000 { compatible = "arm,gic-v3"; #interrupt-cells = <3>; /* 类型、编号、触发方式 */ #address-cells = <2>; #size-cells = <2>; ranges = <0x0 0x2c000000 0x0 0x10000>; /* GICD 寄存器 */ #redistributor-regions = <1>; redistribitor@2c010000 { reg = <0x0 0x2c010000 0x0 0x200000>; /* 每 CPU 一个 GICR */ }; }; /* ITS 翻译服务节点 */ its: msi-controller@2c100000 { compatible = "arm,gic-v3-its"; reg = <0x0 0x2c100000 0x0 0x20000>; /* ITS 寄存器空间 */ msi-parent = <&gic>; /* LPI 由 GIC 分发 */ #msi-cells = <1>; /* MSI 消息只带一个数据字段 */ };属性说明:#msi-cells = <1>表示设备发出的 MSI 消息只有一个 32 位数据字段(PCIe 标准格式);若你的平台用地址低位携带 DeviceID 信息,按绑定文档可写<2>。外设侧不用写特殊内容,驱动通过of_msi_remap或标准 MSI 接口申请即可,内核会自动把它挂到 ITS domain 上走 LPI。GICv3 与 ITS 节点各属性的完整定义见 arm,gic-v3.yaml。
五、调优三板斧:亲和性、优先级、多 ITS
翻译表建好只是起点,性能好不好看这三件事。
中断亲和性设置(/proc/irq调优命令)。LPI 的亲和性最终落在 Collection Table 里,所以内核改亲和性是真在改硬件表,不是软件把戏:
# 把中断 8123 绑到 CPU0 和 CPU1(掩码 0x3) echo 3 > /proc/irq/8123/smp_affinity # 查看当前绑定的 CPU 列表 cat /proc/irq/8123/smp_affinity_list多队列网卡或 NVMe 场景下,把各队列的中断均匀撒到各核,比全挤在一颗核上跑得快得多。
优先级设置。GICv3 的优先级是 8 位,共 256 级(0xff 最低,0x00 最高),通过 GICD 的 PRIORITY 寄存器调整;LPI 的优先级由内核管理,你更常操作的是 SPI 的优先级和抢占关系。设备树里中断单元格的第三个字段是触发方式(IRQ_TYPE_*),不是优先级,别写混。
多 ITS 实例分担。大系统里多个 ITS 实例并行工作,内核按设备的 MPIDR 归属把设备分给不同 ITS,天然做了分片。设备树里给每个 ITS 各写一个节点(同第四节的写法、不同 reg),驱动会全部探测注册,翻译负载随之分摊。
六、收尾:关键概念对照表
| 概念 | 一句话定位 | 容易混的点 |
|---|---|---|
| SPI | 外设直连的固定中断线路,32~1023 共 1020 个 | 全系统共享池,用完就没了 |
| LPI | MSI 消息经 ITS 翻译出来的中断,INTID 可达 2^31 | 不是线路,是"翻译产物",天然支持动态路由 |
| PPI / SGI | 每核私有中断 / CPU 间软件中断 | 和 MSI 路由无关,走传统路径 |
| Device Table | 按 DeviceID 找设备翻译表基址 | 一级入口,对应 MAPD 命令 |
| ITT | 按 EventID 查 LPI 中断号 | 真正的"号码对照表",对应 MAI 命令 |
| Collection Table | 按 INTID 决定 LPI 发给哪些 CPU | 亲和性的硬件落点,对应 MOV 命令 |
| MSI domain | 内核里 ITS 对外的服务接口 | 设备驱动不感知 ITS 细节,只调标准 MSI 接口 |
想继续往下读,从这三个地方入手:
- GICv3 驱动主体:drivers/irqchip/irq-gic-v3.c
- ITS 驱动全部实现(约 5900 行):drivers/irqchip/irq-gic-v3-its.c
- 设备树绑定定义:Documentation/devicetree/bindings/interrupt-controller/arm,gic-v3.yaml
ITS 把"中断号"从稀缺的硬件资源变成了内存里的表项,代价是三张表加一条命令队列的复杂度。理解了这张转接台,GICv4 的虚拟 LPI、虚拟化中断直通也就有了着落点。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考