本文梳理 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
2871 if (only_one_child(bus) && (devfn > 0)) 2872 return 0; 2873 2874 do { 2875 dev = pci_scan_single_device(bus, devfn + fn); 2876 if (dev) { 2877 if (!pci_dev_is_added(dev)) 2878 nr++;
2879 if (fn > 0) 2880 dev->multifunction = 1; 2881 } else if (fn == 0) {
2882 if (!hypervisor_isolated_pci_functions()) 2883 break; 2884 }
2885 fn = next_fn(bus, dev, fn); 2886 } while (fn >= 0); 2887
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
2782 dev = pci_get_slot(bus, devfn); 2783 if (dev) { 2784 pci_dev_put(dev); 2785 return dev; 2786 } 2787
2788 dev = pci_scan_device(bus, devfn); 2789 if (!dev) 2790 return NULL; 2791 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 {
2821 if (pci_ari_enabled(bus)) 2822 return next_ari_fn(bus, dev, fn); 2823 2824 if (fn >= 7) 2825 return -ENODEV; 2826 2827 if (dev && !dev->multifunction) 2828 return -ENODEV; 2829 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 2804 if (!dev) 2805 return -ENODEV; 2806
2807 pos = pci_find_ext_capability(dev, PCI_EXT_CAP_ID_ARI); 2808 if (!pos) 2809 return -ENODEV; 2810
2811 pci_read_config_word(dev, pos + PCI_ARI_CAP, &cap); 2812 next_fn = PCI_ARI_CAP_NFN(cap);
2813 if (next_fn <= fn) 2814 return -ENODEV; 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 重试、空槽过滤、多功能位实测覆盖等细节都封装在下层,让主循环保持简洁。