Linux 内核 PCI 枚举:pci_scan_slot 及其调用链梳理

本文梳理 Linux 内核 PCI 枚举过程中 pci_scan_slot 函数及其调用链的实现。

本文中所有代码引用均基于内核 v7.2-rc2-22-g0e35b9b6ec0f 的源码。【Claude-4.7-Opus】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pci_scan_slot(bus, devfn=D×8)
└─► pci_scan_single_device(bus, D×8+fn) 单 function 扫描
├─► pci_get_slot(bus, D×8+fn) 查 bus->devices 是否已有(冷启动几乎必为 NULL)
└─► pci_scan_device(bus, D×8+fn) 真正探测
├─► pci_bus_read_dev_vendor_id ─► pci_bus_generic_read_dev_vendor_id
│ ├─ 读事务失败? → 无设备
│ ├─ 4 种空槽/坏板返回值? → 无设备
│ ├─ RRS(低16bit=0x0001)? → pci_bus_wait_rrs 指数退避重试
│ └─ 都不是 → 拿到真实 Device|Vendor
├─► pci_alloc_dev 分配 struct pci_dev
└─► pci_setup_device 读 Header/BAR/Class/Cap 链、Header Type 多功能位等
└─► pci_device_add(dev, bus)
├─ list_add_tail(&dev->bus_list, &bus->devices) 挂总线设备链表
└─ device_add(&dev->dev) 进 driver core,触发 match/probe

0.1. 一、pci_scan_slot 函数解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
2866 int pci_scan_slot(struct pci_bus *bus, int devfn)
2867 {
2868 struct pci_dev *dev;
2869 int fn = 0, nr = 0;
2870
/* 早退优化:only_one_child(bus) 为真且 devfn>0 直接返回 0。
* 命中场景:桥是 PCIe Downstream Port(且未设 PCI_SCAN_ALL_PCIE_DEVS)。
* PCIe spec r3.1 §7.3.1 规定这种 Link 下只可能有 Device 0,
* 所以 devfn>0 位置根本不用探,省 32×N 次配置读。 */
2871 if (only_one_child(bus) && (devfn > 0))
2872 return 0; /* Already scanned the entire slot */
2873
/* 主循环:从 fn=0 开始逐 function 扫 devfn+fn。 */
2874 do {
/* 扫描单 function:交给 pci_scan_single_device。 */
2875 dev = pci_scan_single_device(bus, devfn + fn);
2876 if (dev) {
2877 if (!pci_dev_is_added(dev))
2878 nr++; /* nr 累计本次新增设备数,已存在的不计 */
/* 多功能标记:一旦在 fn>0 处扫到设备,说明 fn=0 是多功能设备,
* 回填 multifunction=1。严格说 fn=0 的 multifunction 位应从
* Header Type bit7 读出,这里是兜底/一致性更新。 */
2879 if (fn > 0)
2880 dev->multifunction = 1;
2881 } else if (fn == 0) {
/* fn=0 缺失就退出:fn=0 无设备意味着整个 slot 空,直接 break。
* 例外:hypervisor_isolated_pci_functions() 为真(虚机场景,
* hypervisor 可能把非 0 function 单独直通),此时继续探 fn=1..7。 */
2882 if (!hypervisor_isolated_pci_functions())
2883 break;
2884 }
/* next_fn 决定下一个 fn:
* 传统 PCI → fn+1(到 7 结束)
* ARI 使能 → 跳到 ARI Cap 的 Next Function Number(fn 可到 255) */
2885 fn = next_fn(bus, dev, fn);
2886 } while (fn >= 0);
2887
/* 若本 slot 挂在某个 bridge 下且真的扫到了新设备,
* 初始化该桥的 ASPM 状态(等 Link 下所有 endpoint 都扫完再统一决策)。 */
2888 if (bus->self && nr)
2889 pcie_aspm_init_link_state(bus->self);
2890
2891 return nr; /* 返回本次新增设备数 */
2892 }

关键约束pci_scan_slot 每一次 loop 内只处理同一个 Device 的一个 Function;跨 Device 的迭代由外层(pci_scan_child_bus_extend)通过对 devfn=0, 8, 16, ... 分别调 pci_scan_slot 完成。

0.2. 二、pci_scan_single_device:先查后扫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2778 struct pci_dev *pci_scan_single_device(struct pci_bus *bus, int devfn)
2779 {
2780 struct pci_dev *dev;
2781
/* 第一段:查 bus->devices 是否已经挂载过该 devfn。
* 冷启动路径下这里必为 NULL;
* 主要服务热插拔 / rescan 语义——同一 (bus, devfn) 反复扫描时
* 不会重复挂载,保证幂等。 */
2782 dev = pci_get_slot(bus, devfn);
2783 if (dev) {
2784 pci_dev_put(dev); /* pci_get_slot 会 get 引用,直接返回时要 put 掉 */
2785 return dev;
2786 }
2787
/* 第二段:真正探测硬件、分配 pci_dev、读 Header/BAR/Cap 链。
* 探测过程中的空槽过滤和 RRS 重试封装在更下层。 */
2788 dev = pci_scan_device(bus, devfn);
2789 if (!dev)
2790 return NULL; /* 探不到设备,直接返回 */
2791
/* 挂到 bus->devices 链表,并 device_add 到 driver core,触发 match/probe。 */
2792 pci_device_add(dev, bus);
2793
2794 return dev;
2795 }

0.3. 三、next_fn / next_ari_fn:两种”下一 function”策略

ARI 的背景、动机与内核使能路径详见上一篇:《PCIe ARI(Alternative Routing-ID Interpretation):为什么需要 & 如何工作》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2819 static int next_fn(struct pci_bus *bus, struct pci_dev *dev, int fn)
2820 {
/* ARI 模式:走 next_ari_fn 稀疏跳转,允许 fn ∈ [0,255],
* 不再受 3-bit function 字段的 8 上限约束。 */
2821 if (pci_ari_enabled(bus))
2822 return next_ari_fn(bus, dev, fn);
2823
/* 传统 PCI:3-bit function 字段最多到 7,到顶就结束。 */
2824 if (fn >= 7)
2825 return -ENODEV;
2826 /* only multifunction devices may have more functions */
/* dev.0 Header Type bit7=0 表示单功能设备,其它 fn 直接不用探。 */
2827 if (dev && !dev->multifunction)
2828 return -ENODEV;
2829
/* 传统模式下就是简单地 fn+1,从 0 顺序探到 7。 */
2830 return fn + 1;
2831 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2798 static int next_ari_fn(struct pci_bus *bus, struct pci_dev *dev, int fn)
2799 {
2800 int pos;
2801 u16 cap = 0;
2802 unsigned int next_fn;
2803
/* 没有当前 dev(fn=0 都没扫到)就无从读 ARI Cap,直接结束。 */
2804 if (!dev)
2805 return -ENODEV;
2806
/* 从 Extended Cap 空间(配置空间偏移 ≥0x100)里查找 ARI Capability。
* 找不到说明本 function 不支持 ARI,链到此为止。 */
2807 pos = pci_find_ext_capability(dev, PCI_EXT_CAP_ID_ARI);
2808 if (!pos)
2809 return -ENODEV;
2810
/* 读 ARI Capability 寄存器,取其中的 NFN(Next Function Number)字段:
* 硬件直接告诉内核"下一个存在的 function 号",跳过所有空 function,
* 避免挨个尝试最多 256 次配置读——SR-IOV 大量 VF 场景的关键性能优化。 */
2811 pci_read_config_word(dev, pos + PCI_ARI_CAP, &cap);
2812 next_fn = PCI_ARI_CAP_NFN(cap);
/* NFN ≤ 当前 fn 视为"链表异常 / 结束":
* 末尾 function 的 NFN 通常填 0,天然承担了"ARI 链结束标记"的作用。 */
2813 if (next_fn <= fn)
2814 return -ENODEV; /* protect against malformed list */
2815
2816 return next_fn;
2817 }

0.4. 四、几个关键设计点

4.1 “slot 单位 = 一个 Device”:外层调用 pci_scan_slot(bus, 0), pci_scan_slot(bus, 8), pci_scan_slot(bus, 16), …;每次 devfn 必须是 8 的整数倍(对应 dev.0)。这也解释了函数注释里 “must have zero function” 的约束。

4.2 fn=0 是”看门人”:非虚拟化场景下,fn=0 空即视为整个 slot 空,避免对 32×8=256 个位置逐一试探。

4.3 多功能位以”实测”覆盖”申报”:即使 dev.0 声称自己非多功能,只要在 fn>0 上真扫到设备(hypervisor 直通场景),仍会强制置 multifunction=1,保持内核视图一致。

4.4 ARI 与传统在同一函数里无缝切换next_fn 一处开关(pci_ari_enabled(bus)),把 8-function 硬顶抬到 256-function,且完全不影响 pci_scan_slot 主循环。

4.5 RRS 由更底层的 pci_bus_generic_read_dev_vendor_id 兜住pci_scan_slot 层面看到的永远只是”有 / 无 dev”两态,重试逻辑对它透明。

4.6 ASPM 初始化时机:放在 slot 扫完之后、发现有新设备时——因为 ASPM 状态是 Link 两端共同决定的,要等 Link 下所有 endpoint 都被扫到并读完 Cap 后再统一决策一次,避免”一边探一边改 Link 状态”。

0.5. 五、小结

  • 定位pci_scan_slot 是 PCI 枚举里”扫一个 slot 的所有 function”的基本单位
  • 遍历:以 fn=0 为看门人,fn=0 空即视为整个 slot 空,避免逐个位置试探。
  • 探测:通过 pci_scan_single_device(先查后扫)逐个探测每一个 function。
  • next one:用 next_fn 在传统 8-function 与 ARI 256-function 两种模式之间无缝切换。
  • 链表:把新发现的 pci_dev 挂到 bus->devices 链表,并接入 driver core(触发 match / probe)。
  • 细节:RRS 重试、空槽过滤、多功能位实测覆盖等细节都封装在下层,让主循环保持简洁。

Linux 内核 PCI 枚举:pci_scan_slot 及其调用链梳理
https://martinbj2008.github.io/2026/07/09/2026-07-09-pci-scan-slot.ai/
Author
Martinbj2008
Posted on
July 9, 2026
Licensed under