Linux 下 pcie 初始化设备枚举流程代码分析

📅 2026/8/17 16:53:36
Linux 下 pcie 初始化设备枚举流程代码分析
1、简介以 rk3568 pcie 代码为例简要介绍一下 pcie 初始化设备枚举的过程。比较重要的函数就是pci_scan_child_bus_extend、pci_scan_bridge_extend这两个函数是递归的核心。简要函数调用流程如下2、pci_scan_child_bus_extend该函数入参 bus 为 pci_bus 结构表示一个总线资源。该函数首次被调用时传递下来的是 root bus 结构该函数作用为扫描并创建当前 bus 入参上的所有 pci 设备可能是 endpoint 设备也可能是桥设备当前 bus 扫描结束调用pci_scan_bridge_extend尝试遍历下一级 bus返回值为新的 subordinate number根据不断递归获取到当前 bus 下的最最深一级的 bus 号/** * pci_scan_child_bus_extend() - Scan devices below a bus * bus: Bus to scan for devices * available_buses: Total number of buses available (%0 does not try to * extend beyond the minimal) * * Scans devices below bus including subordinate buses. Returns new * subordinate number including all the found devices. Passing * available_buses causes the remaining bus space to be distributed * equally between hotplug-capable bridges to allow future extension of the * hierarchy. */staticunsignedintpci_scan_child_bus_extend(structpci_bus*bus,unsignedintavailable_buses){....../* Go find them, Rover! *//* 这里的含义是遍历当前 bus 上的所有设备。针对遍历到的设备创建 pci_dev 结构挂载 pci_bus 结构的链表上 */for(devfn0;devfn256;devfn8){/* * 该函数会去调用 pci_scan_single_device创建、初始化 pci 设备包括 endpoint 设备与桥设备 * 包括但不限于 BAR 空间的初始化、中断资源的初始化、设备 capability 使能等 */nr_devspci_scan_slot(bus,devfn);/* * The Jailhouse hypervisor may pass individual functions of a * multi-function device to a guest without passing function 0. * Look for them as well. */if(jailhouse_paravirt()nr_devs0){for(fn1;fn8;fn){devpci_scan_single_device(bus,devfnfn);if(dev)dev-multifunction1;}}}/* Reserve buses for SR-IOV capability *//* 还记得前面 SR-IOV 章节的保留 bus 号么没有阅读的可以先去阅读下 */used_busespci_iov_bus_range(bus);maxused_buses;....../* * Scan bridges that are already configured. We dont touch them * unless they are misconfigured (which will be done in the second * scan below). *//* for 循环这里是遍历当前 bus 上的所有桥设备不包括 endpoint 设备*/for_each_pci_bridge(dev,bus){cmaxmax;/* 这里是为了处理 BIOS/Boot 中已经被配置好的 pci 桥 这个是为了兼容各个架构所做的妥协 */maxpci_scan_bridge_extend(bus,dev,max,0,0);/* * Reserve one bus for each bridge now to avoid extending * hotplug bridges too much during the second scan below. */used_buses;if(cmax-max1)used_busescmax-max-1;}/* Scan bridges that need to be reconfigured *//* for 循环这里是遍历当前 bus 上的所有桥设备不包括 endpoint 设备 */for_each_pci_bridge(dev,bus){unsignedintbuses0;if(!hotplug_bridgesnormal_bridges1){/* * There is only one bridge on the bus (upstream * port) so it gets all available buses which it * can then distribute to the possible hotplug * bridges below. */busesavailable_buses;}elseif(dev-is_hotplug_bridge){/* * Distribute the extra buses between hotplug * bridges if any. */busesavailable_buses/hotplug_bridges;busesmin(buses,available_buses-used_buses1);}cmaxmax;/* 这里才是真的是递归遍历下一级 bus通过 buses 参数传递 bus 号 */maxpci_scan_bridge_extend(bus,dev,cmax,buses,1);/* One bus is already accounted so dont add it again */if(max-cmax1)used_busesmax-cmax-1;}......}3、pci_scan_slotpci_scan_child_bus_extend()以 8 为步长遍历devfn每次调用pci_scan_slot()扫描一个 Device Number。传入的devfn低 3 位必须为 0即扫描总是从 Function 0 开始如果 Function 0 表明该设备支持多个 Function函数还会继续枚举同一 Device 下的其他 Function。扫描到的每个 Function 都由pci_scan_single_device()创建或获取对应的pci_dev并挂入bus-devices链表。因此pci_scan_slot()的职责可以概括为以 Device 为单位确定扫描范围以 Function 为粒度发现并登记设备。其返回值nr表示本次扫描中新发现的 Function 数量而不是该总线上已有设备的总数。/** * pci_scan_slot - Scan a PCI slot on a bus for devices * bus: PCI bus to scan * devfn: slot number to scan (must have zero function) * * Scan a PCI slot on the specified PCI bus for devices, adding * discovered devices to the bus-devices list. New devices * will not have is_added set. * * Returns the number of new devices found. */intpci_scan_slot(structpci_bus*bus,intdevfn){unsignedfn,nr0;structpci_dev*dev;/* PCIe 下行链路通常只允许出现 Device 0无需继续扫描其他 Device */if(only_one_child(bus)(devfn0))return0;/* Already scanned the entire slot *//* 首先扫描 Function 0它决定后续是否需要扫描其他 Function */devpci_scan_single_device(bus,devfn);if(!dev)return0;if(!pci_dev_is_added(dev))nr;/* 枚举传统多功能设备或 ARI 设备中的后续 Function */for(fnnext_fn(bus,dev,0);fn0;fnnext_fn(bus,dev,fn)){devpci_scan_single_device(bus,devfnfn);if(dev){if(!pci_dev_is_added(dev))nr;dev-multifunction1;}}/* Only one slot has PCIe device */if(bus-selfnr)pcie_aspm_init_link_state(bus-self);returnnr;}函数的执行过程可分为四个阶段裁剪 Device 扫描范围。only_one_child()判断当前总线是否位于 PCIe 下行端口之后。若该链路按照 PCIe 拓扑只可能出现 Device 0则后续针对 Device 131 的扫描直接返回。探测 Function 0。pci_scan_single_device()首先访问当前 Device 的 Function 0。若 Function 0 不存在说明该 Device 不存在当前槽位的扫描随即结束。枚举后续 Function。next_fn()决定下一个 Function Number。对于传统多功能设备它依据 Header Type 中的 Multifunction 位扫描 Function 17并允许 Function Number 不连续启用 ARIAlternative Routing-ID Interpretation后则根据 ARI Capability 中的 Next Function Number 继续枚举。完成链路初始化。如果本次发现了新设备且当前总线存在上游桥bus-self则调用pcie_aspm_init_link_state()初始化这条 PCIe 链路的 ASPM 状态。3.1 PCI 核心层的扫描范围裁剪传统 PCI 总线采用共享总线模型同一条总线上可以合法挂接多个 Device因此必须扫描 Device 031。PCIe Root Port 或 Downstream Port 的下游侧则是一条点对点链路链路对端通常只能以 Device 0 出现这个 Device 既可能是 Endpoint也可能是 PCIe Switch 的 Upstream Port。Linux PCI 子系统通过only_one_child()判断当前总线的上游桥是否为面向下游的 PCIe 端口并据此将该总线的扫描范围裁剪到 Device 0。only_one_child()既减少了对不存在 BDF 的无效探测也避免部分控制器因与实际拓扑不符的配置事务进入异常上报或超时处理路径。如果没有该步骤pci_scan_child_bus_extend()仍会依次调用pci_scan_slot()探测 Device 131并最终通过 Host 控制器发起真实的配置空间访问这些 Device 在当前链路上虽然必然不存在但访问产生的结果取决于控制器的具体实现。对于实现完备的控制器不存在的 BDF 应当安全返回全 1 或PCIBIOS_DEVICE_NOT_FOUND此时only_one_child()主要体现为枚举效率的提升。但在部分硬件上无效访问还可能引发 Completion Timeout、系统总线异常甚至因地址译码问题映射到其他设备的配置空间。因此这项裁剪同时具有降低风险的保护作用。这里限制的是Device Number并不意味着链路对端只能包含一个 Function。Device 0 仍然可以是传统多功能设备也可以在启用 ARI 后提供更多 Function这些 Function 由后续的next_fn()继续枚举。对于确实存在特殊拓扑的平台可以设置PCI_SCAN_ALL_PCIE_DEVS要求 PCI 核心扫描全部 Device Number。staticinlineboolpcie_downstream_port(conststructpci_dev*dev){inttypepci_pcie_type(dev);returntypePCI_EXP_TYPE_ROOT_PORT||typePCI_EXP_TYPE_DOWNSTREAM||typePCI_EXP_TYPE_PCIE_BRIDGE;}staticintonly_one_child(structpci_bus*bus){structpci_dev*bridgebus-self;/* 特殊拓扑要求扫描全部 Device Number */if(pci_has_flag(PCI_SCAN_ALL_PCIE_DEVS))return0;/* PCIe 下行端口之后通常只有 Device 0 */if(bridgepci_is_pcie(bridge)pcie_downstream_port(bridge))return1;return0;}3.2 Host 控制器驱动的配置空间访问过滤only_one_child()依赖bus-self判断当前总线的上游桥而 Root Bus 没有上游桥因此其bus-self为NULL。同时PCI_SCAN_ALL_PCIE_DEVS可以要求跳过这项裁剪配置空间访问也可能来自重新扫描、热插拔或其他内核路径。因此Host 控制器驱动必须结合自身的 Root Bus 拓扑和地址译码方式在pci_ops入口独立检查目标 BDF 是否可以安全访问。staticintrockchip_pcie_valid_device(structrockchip_pcie*rockchip,structpci_bus*bus,intdev){/* * Access only one slot on each root port. * Do not read more than one device on the bus directly attached * to RCs downstream side. */if(pci_is_root_bus(bus)||pci_is_root_bus(bus-parent))returndev0;return1;}这项检查不仅用于减少无效访问还直接关系到枚举结果的正确性。Root Bus 上的配置空间访问最终进入rockchip_pcie_rd_own_conf()而该函数只根据寄存器偏移where访问 RC 内 Root Port 的 own configuration space调用链中并没有把devfn传递给它staticintrockchip_pcie_rd_conf(structpci_bus*bus,u32 devfn,intwhere,intsize,u32*val){structrockchip_pcie*rockchipbus-sysdata;if(!rockchip_pcie_valid_device(rockchip,bus,PCI_SLOT(devfn))){*val0xffffffff;returnPCIBIOS_DEVICE_NOT_FOUND;}if(pci_is_root_bus(bus))returnrockchip_pcie_rd_own_conf(rockchip,where,size,val);returnrockchip_pcie_rd_other_conf(rockchip,bus,devfn,where,size,val);}staticintrockchip_pcie_rd_own_conf(structrockchip_pcie*rockchip,intwhere,intsize,u32*val){void__iomem*addr;addrrockchip-apb_basePCIE_RC_CONFIG_NORMAL_BASEwhere;......}这意味着如果没有rockchip_pcie_valid_device()Root Bus 上针对 Device 131 的读操作也会落到同一份 Root Port 配置空间并读出与 Device 0 相同的 Vendor ID、Device ID 等信息。Linux PCI 子系统可能由此把同一个 Root Port 重复识别为多个桥设备进一步引发总线号和资源窗口冲突、重复初始化等问题对非法 Device Number 的写操作则可能直接改写 Root Port 自身的配置寄存器。因此Root Bus 上的过滤属于保证正确性的必要措施而不仅仅是性能优化。该函数在rockchip_pcie_rd_conf()和rockchip_pcie_wr_conf()的入口处被调用访问 Root Bus 时仅允许访问 Device 0即 RC 内 Root Port 的配置空间访问 Root Bus 的直接子总线时同样只允许访问 Device 0即 RC 下行链路的唯一对端从配置空间访问入口阻止与实际拓扑不符的事务对更深层级的总线不作此限制因为 PCIe Switch 下游可能形成包含多个 Device Number 的合法拓扑。如果目标 Device Number 不合法读操作返回0xffffffff同时以PCIBIOS_DEVICE_NOT_FOUND结束不再向硬件发起配置事务。由此可见only_one_child()是 Linux PCI 子系统依据 PCIe 拓扑实施的第一层扫描裁剪rockchip_pcie_valid_device()则是 Host 驱动在硬件访问入口设置的第二道防线。前者避免探测拓扑上确定不存在的 Device后者负责阻止地址混叠及控制器不支持的配置事务两者不能相互替代。4、pci_scan_single_devicepci_scan_single_device函数是初始化 pcie 设备的重中之重。本篇其余部分都是 pcie 设备枚举的过程只有这个函数是配置函数。structpci_dev*pci_scan_single_device(structpci_bus*bus,intdevfn){structpci_dev*dev;devpci_get_slot(bus,devfn);if(dev){pci_dev_put(dev);returndev;}/* * 为当前设备创建 pci_dev 结构 * 同时初始化当前设备的 BAR 资源、中断资源 */devpci_scan_device(bus,devfn);if(!dev)returnNULL;/* 初始化当前设备的 capabilities 功能 */pci_device_add(dev,bus);returndev;}pci_device_add()-pci_init_capabilities()-pci_ea_init -pci_configure_ari -pci_iov_init -......5、pci_scan_bridge_extend该函数入参 bus 为 pci_bus 结构表示一个总线资源。该函数首次被调用时传递下来的是 root bus 结构该函数作用为扫描并创建当前 bus 入参的下级 bus在pci_add_new_bus函数中会去创建新的 bus 结构dev 结构是 pci 设备的 pci_dev 结构。注意这里的 pci 设备只会是 pci 桥设备不会是 endpoint 设备返回值为新的 subordinate number根据不断递归获取到当前 bus 下的最最深一级的 bus 号/* * pci_scan_bridge_extend() - Scan buses behind a bridge * bus: Parent bus the bridge is on * dev: Bridge itself * max: Starting subordinate number of buses behind this bridge * available_buses: Total number of buses available for this bridge and * the devices below. After the minimal bus space has * been allocated the remaining buses will be * distributed equally between hotplug-capable bridges. * pass: Either %0 (scan already configured bridges) or %1 (scan bridges * that need to be reconfigured. * * If its a bridge, configure it and scan the bus behind it. * For CardBus bridges, we dont scan behind as the devices will * be handled by the bridge driver itself. * * We need to process bridges in two passes -- first we scan those * already configured by the BIOS and after we are done with all of * them, we proceed to assigning numbers to the remaining buses in * order to avoid overlaps between old and new bus numbers. * * Return: New subordinate number covering all buses behind this bridge. */staticintpci_scan_bridge_extend(structpci_bus*bus,structpci_dev*dev,intmax,unsignedintavailable_buses,intpass){......pci_read_config_dword(dev,PCI_PRIMARY_BUS,buses);primarybuses0xFF;secondary(buses8)0xFF;subordinate(buses16)0xFF;....../* 这里的 if 分支含义是在 BIOS/Boot 没有配置的情况下当前桥设备 pci_dev 的配置空间读出非 0 */if((secondary||subordinate)!pcibios_assign_all_busses()!is_cardbus!broken){unsignedintcmax;/* * Bus already configured by firmware, process it in the * first pass and just note the configuration. */if(pass)gotoout;/* * The bus might already exist for two reasons: Either we * are rescanning the bus or the bus is reachable through * more than one bridge. The second case can happen with * the i450NX chipset. *//* 这里会去为下一级 bus 创建 pci_bus 结构下一级 bus 的 bus 号为 secondary */childpci_find_bus(pci_domain_nr(bus),secondary);if(!child){childpci_add_new_bus(bus,dev,secondary);if(!child)gotoout;child-primaryprimary;pci_bus_insert_busn_res(child,secondary,subordinate);child-bridge_ctlbctl;}/* 递归入口这里的 child 已经下一级的 bus 了 */cmaxpci_scan_child_bus(child);if(cmaxsubordinate)pci_warn(dev,bridge has subordinate %02x but max busn %02x\n,subordinate,cmax);/* Subordinate should equal child-busn_res.end */if(subordinatemax)maxsubordinate;}else{/* 这里的 else 分支含义是在 BIOS/Boot 已经配置的情况下或者当前桥设备 pci_dev 的配置空间读出为 0 *//* * We need to assign a number to this bus which we always * do in the second pass. */if(!pass){if(pcibios_assign_all_busses()||broken||is_cardbus)/* * Temporarily disable forwarding of the * configuration cycles on all bridges in * this bus segment to avoid possible * conflicts in the second pass between two * bridges programmed with overlapping bus * ranges. *//* * 这里是为了解决 bus 号冲突问题。因为 BIOS 已经配置好桥设备的 bus 资源 * 但因为现在操作系统又在重新配置可能会和 BIOS 原先的配置有冲突 * 所以这里对 bus 资源先进行了一个复位操作全写 0 */pci_write_config_dword(dev,PCI_PRIMARY_BUS,buses~0xffffff);gotoout;}/* Clear errors */pci_write_config_word(dev,PCI_STATUS,0xffff);/* Read bus numbers from EA Capability (if present) */fixed_busespci_ea_fixed_busnrs(dev,fixed_sec,fixed_sub);if(fixed_buses)next_busnrfixed_sec;elsenext_busnrmax1;/* 更新下一级 bus 的 bus 号 *//* * Prevent assigning a bus number that already exists. * This can happen when a bridge is hot-plugged, so in this * case we only re-scan this bus. *//* 这里会去为下一级 bus 创建 pci_bus 结构下一级 bus 的 bus 号为 next_busnr*/childpci_find_bus(pci_domain_nr(bus),next_busnr);if(!child){childpci_add_new_bus(bus,dev,next_busnr);if(!child)gotoout;pci_bus_insert_busn_res(child,next_busnr,bus-busn_res.end);}max;if(available_buses)available_buses--;/* 这里会去更新当前桥设备的 pri、sec、sub 寄存器这里的 sub 还默认是 0xff */buses(buses0xff000000)|((unsignedint)(child-primary)0)|((unsignedint)(child-busn_res.start)8)|((unsignedint)(child-busn_res.end)16);....../* We need to blast all three values with a single write */pci_write_config_dword(dev,PCI_PRIMARY_BUS,buses);if(!is_cardbus){child-bridge_ctlbctl;/* 递归入口这里的 child 已经下一级的 bus 了 */maxpci_scan_child_bus_extend(child,available_buses);}else{......}/* * Set subordinate bus number to its real value. * If fixed subordinate bus number exists from EA * capability then use it. */if(fixed_buses)maxfixed_sub;pci_bus_update_busn_res_end(child,max);/* 递归结束会根据递归得到的 max 值去修改 sub 寄存器的值 */pci_write_config_byte(dev,PCI_SUBORDINATE_BUS,max);}......}5、举例以上面这张图举例Linux 下的 PCIe 设备枚举顺序为调用pci_scan_child_bus_extend创建 bus 2 上的设备创建出 root bus 设备200调用pci_scan_bridge_extend创建下一级 bus 3再次调用pci_scan_child_bus_extend创建 bus 3 上的设备创建出 upstream port300[这里开始第一次递归]再调用pci_scan_bridge_extend创建下一级 bus 4再次调用pci_scan_child_bus_extend创建 bus 4 上的设备创建出 downstream port410、420…4180 [这里开始第二次递归]要注意这里会一次性创建出当前 bus 上所有扫描到的设备。这里已经涉及到了广度优先遍历再调用pci_scan_bridge_extend创建下一级 bus 5再次调用pci_scan_child_bus_extend创建 bus 5 上的设备创建出 endpoint 设备500[这里开始第一次递归返回返回的位置就是 for_each_pci_bridge 这个循环]如果 500是一个 switch 的话当尝试创建 bus 6 时实际上就等同于创建 upstream port 300的过程这时会接着往下深度遍历不会往右遍历。遍历结束根据遍历的返回值也就是递归的返回值原图中的 bus 6这时就有可能变成 bus 8、bus 9…再次调用pci_scan_child_bus_extend创建 bus 6 上的设备创建出 endpoint 设备600…从上面的分析我们不难看出Linux 内核代码中PCIe 设备枚举的过程不仅仅是深度优先遍历中间还夹杂了广度优先遍历这一点不看代码是很难知道的网上能搜到的结论都是清一色的深度优先遍历。纸上得来终觉浅绝知此事要躬行鸭