PCIe ARI(Alternative Routing-ID Interpretation):为什么需要 & 如何工作
本文整理了 PCIe ARI 的背景动机与技术方法,以及 Linux 内核在 PCI 枚举扫描过程中对 ARI 的使用方式。
讨论主线:传统 PCI BDF 的 8-function 限制 → SR-IOV 大量 VF 的编址困境 → ARI 的解决方案 → 内核实现细节。
本文中所有代码引用均基于内核 v7.2-rc2-22-g0e35b9b6ec0f 的源码。【Claude-4.7-Opus】
0.1. 一、背景:为什么需要 ARI
0.1.1. 1.1 传统 PCI 的 BDF 编址
PCI 配置事务里,Routing ID(RID)总长 16 bit,用来在总线上唯一寻址一个 function:
1 | |
- Bus:8 bit,0..255;
- Device:5 bit,一条 bus 最多 32 个 slot;
- Function:3 bit,一个 Device 最多 8 个 function。
这就是”一个 D 下最多 8 个 function”的硬性来源——协议字段就 3 bit,不可能表达超过 8 的 function 号。
0.1.2. 1.2 SR-IOV 的编址困境
SR-IOV(Single Root I/O Virtualization)允许一个物理设备(PF, Physical Function)虚拟出很多个 VF(Virtual Function),每个 VF 都是标准的 PCI Function:
- 有自己独立的 Configuration Space;
- 有自己独立的 BAR;
- 有自己独立的中断;
- 可以被独立地 passthrough 给虚拟机。
问题来了:一块现代网卡(如 Mellanox ConnectX、Intel 82599 后继型号)常常需要 64/128/256 个 VF,而传统 3-bit function 字段最多只能编址 8 个 function,根本装不下。
如果只靠”多条 Bus 号分散”来放 VF:128 个 VF 就得至少占 16 条 bus,效率低,且浪费宝贵的 Bus 号资源。
ARI 就是为了解决这个编址困境而设计的。
0.2. 二、方法:ARI 的核心思想
0.2.1. 2.1 重新解释 Routing ID
ARI 使能后,在支持 ARI 的 bus 上,Routing ID 低 8 bit 不再拆成 5 bit Device + 3 bit Function,而是整体作为 Function 号:
1 | |
关键变化:
- ARI bus 上,Device 号概念被”抹掉”,全部 8 bit 都当 function 号;
- 一条 bus 上理论上最多 256 个 function(0..255);
- 代价:一条 ARI bus 上只能有一个”逻辑设备”(因为 Device 域不再存在)。
0.2.2. 2.2 ARI 使能的两个必要条件
必须同时满足:
- 上游桥支持并使能 ARI Forwarding(PCIe Capability 里的
ARI Forwarding Enablebit); - 端点设备支持 ARI(Extended Capability 里有 ARI Capability,报告
Next Function Number)。
只要上游桥或者端点任一方不支持,就退化为传统 Non-ARI 行为。
0.2.3. 2.3 SR-IOV 中 VF 的 Routing ID 计算
SR-IOV Extended Capability 里的关键寄存器:
| 字段 | 含义 |
|---|---|
NumVFs |
当前使能的 VF 数量 |
First VF Offset |
第一个 VF 的 RID 相对 PF 的偏移(16 bit) |
VF Stride |
相邻两个 VF 之间的 RID 步长(16 bit) |
VF 的 BDF 用公式算:
1 | |
Routing ID 一路加下去,会自然溢出到下一条 Bus。
0.3. 三、两种典型场景对照
0.3.1. 场景 A:Non-ARI(如 Intel 82599)
VF 挤在同一条 bus 的高 Device 号上:
1 | |
对应 lspci 输出示例(下方内容为 AI 生成的模拟输出,非实测数据,仅用于说明 BDF 排列规律):
1 | |
0.3.2. 场景 B:ARI 使能(如 Mellanox ConnectX)
VF 直接跨到下一条 bus,从 fn 0 起连续排列:
1 | |
规则:VF BDF = PF RID 加上 First VF Offset 再加上 i × Stride,然后按 ARI/Non-ARI 拆成 Bus + Dev/Fn,一步到位,不存在”先到 04:10.0 再跨到 05:00.0”这种中间态。
0.4. 四、Linux 内核中的 ARI 处理
0.4.1. 4.1 扫描器分支:next_fn
位置:drivers/pci/probe.c
1 | |
三条路径:
- ARI 使能 → 走
next_ari_fn(利用 ARI Cap 的 Next Function Number 稀疏跳跃); - Non-ARI,fn 0 是单 function 设备 →
dev->multifunction == 0,直接-ENODEV; - Non-ARI,多 function 设备 →
fn + 1,直到 fn = 7 停止。
0.4.2. 4.2 ARI 的稀疏跳跃:next_ari_fn
位置:drivers/pci/probe.c
1 | |
优势:ARI Cap 直接告诉内核”下一个存在的 function 号”,跳过所有空 function,避免挨个尝试 256 次配置读。这是 SR-IOV 大量 VF 场景下的关键性能优化。
0.4.3. 4.3 上限对比
| 场景 | 单 bus function 上限 | 由谁决定 |
|---|---|---|
| Non-ARI | 8(fn = 0..7) | if (fn >= 7) return -ENODEV; |
| ARI 使能 | 256(fn = 0..255) | ARI Cap 的 Next Function Number 字段 |
0.5. 五、验证方法(可在真机上执行)
说明:以下命令本身是通用的 Linux 验证套路,可以在支持 SR-IOV 的真机上直接执行。但下方所有 # 期望输出示例 中展示的字段和数值均为 AI 生成的模拟输出,仅用于说明字段结构与 BDF 排列规律,不代表任何一台真实机器的实测数据。
1 | |
对照关系:若 lspci 报告 VF offset: 128,对应 First VF Offset = 0x80;stride: 1 对应 VF Stride = 1。手动套公式算,应该与实机 lspci 输出一致。
0.6. 六、总结对比表
| 维度 | 传统 PCI Function | SR-IOV VF (ARI) |
|---|---|---|
| 本质 | Function | Function(就是 function) |
| Routing ID 结构 | Bus:Dev(5):Fn(3) |
Bus:Fn(8),Dev 域消失 |
| 单 bus function 上限 | 8(3 bit) | 256(8 bit) |
| 单 PF 下 VF 数上限 | — | 由 SR-IOV Cap 的 TotalVFs 决定,规范最大 64K |
| 配置空间 | 完整独立 | 完整独立(Type 0,1 KB / 4 KB) |
| BAR | 自己有 | 自己有(从 SR-IOV Cap 的 VF BAR 派生) |
| 依赖能力 | 无 | 需要 ARI Forwarding + SR-IOV Extended Cap |
| 枚举方式 | next_fn 里 fn+1 |
next_ari_fn 走 ARI Cap 的 Next Function Number |
| 典型 BDF 示例 | 04:00.0..04:00.7 |
PF 04:00.0; VFs 05:00.0..05:00.7f(128 VFs) |
0.7. 七、几个易错点澄清
7.1 “VF 是不是 function?” —— 是。VF 是标准的 PCI Function,只是通过 ARI 摆脱了 3-bit 编号限制。
7.2 “一个 D 下最多 8 个 function” 什么时候成立?
- 传统 PCI / Non-ARI PCIe:✅ 成立(Function 字段就 3 bit);
- ARI PCIe(含 SR-IOV):❌ 不成立。“D 号” 概念在 ARI bus 上被抹掉了,整条 bus 就是一个 256-function 的扁平空间。
7.3 VF BDF 的中间态 —— 不存在。VF 的 BDF 一步算出(PF_RID + offset + i × stride,再按 ARI/Non-ARI 拆 Bus/Dev/Fn),不要写成”先 04:10.0 再跨到 05:00.0”这种自相矛盾的中间态。
0.8. 八、一句话结论
ARI 通过把 BDF 中原本”5 bit Dev + 3 bit Fn”的划分改成”8 bit Fn”(Device 概念在该 bus 上消失),把单 bus function 数从 8 扩到 256;配合 SR-IOV Cap 的 First VF Offset / Stride,VF 可以进一步溢出到新 Bus 号,从而支撑 128 甚至几千个 VF。
Linux 内核在 next_fn 里通过 pci_ari_enabled(bus) 分支进入 next_ari_fn,读取 ARI Cap 的 Next Function Number 做稀疏跳跃,避免枚举时挨个尝试 256 次配置读,是现代 SR-IOV 场景不可或缺的性能优化。