4. Linux PCI 子系统初始化:pci_init() 流程与 pci_scan_bus() 核心逻辑
PCI 子系统的初始化是内核启动过程中将 PCI/PCIe 拓扑从硬件寄存器“翻译”为内核设备模型的关键阶段。本章深入剖析 pci_init() 的调用链路与 pci_scan_bus() 的递归扫描机制,帮助学员建立从入口函数到设备挂载的完整认知。
4.1 pci_init() 初始化流程
pci_init() 是 PCI 子系统的总入口,在 fs_initcall(pci_init) 阶段被调用。其核心职责包括:分配资源、注册驱动、初始化 ACPI 接口、扫描根总线。
关键路径: start_kernel() → rest_init() → kernel_init() → do_basic_setup() → do_initcalls() → pci_init()
4.1.1 主要执行步骤
| 步骤 | 函数/操作 | 说明 |
|---|---|---|
| 1 | pci_apply_final_quirks() |
应用最终阶段的设备 quirks,修正硬件缺陷 |
| 2 | pci_resource_alignment() 初始化 |
设置资源对齐策略,为后续分配做准备 |
| 3 | pci_init_host_bridge() |
初始化主机桥(Host Bridge)数据结构 |
| 4 | acpi_pci_root_init() |
通过 ACPI 表枚举 PCI 根总线(x86/ARM 通用) |
| 5 | pci_scan_bus() 或 pci_scan_root_bus() |
递归扫描总线,发现所有设备与桥 |
| 6 | pci_bus_add_devices() |
将扫描到的设备添加到内核设备模型并触发驱动绑定 |
调试提示: 在
pci_init() 入口处设置 initcall_debug 内核参数,可以打印所有 initcall 的调用顺序与耗时,快速定位 PCI 初始化卡死点。
4.1.2 pci_init() 整体流程 SVG
4.2 pci_scan_bus() 核心逻辑
pci_scan_bus() 是递归扫描的引擎。给定一个总线号,它读取该总线上所有设备的 Vendor ID / Device ID,若有效则创建 pci_dev 结构体,并判断是否为 PCI-to-PCI 桥,若是则递归扫描下游总线。
4.2.1 扫描算法伪代码
pci_scan_bus(bus)
for each devfn (0..255):
if pci_read_config_word(devfn, PCI_VENDOR_ID) != 0xFFFF:
dev = pci_scan_device(bus, devfn)
if dev->hdr_type == PCI_HEADER_TYPE_BRIDGE:
child_bus = pci_add_new_bus(bus, dev, secondary_bus_nr)
pci_scan_bus(child_bus) // 递归扫描下游
pci_assign_unassigned_resources()
pci_bus_add_devices(bus)
常见陷阱: 如果桥的
secondary bus number 寄存器未正确配置(例如为 0),递归扫描会提前终止,导致下游设备丢失。调试时务必检查 lspci -t 输出的总线树结构。
4.2.2 递归扫描流程 SVG
4.3 关键数据结构关系
扫描过程中涉及的核心数据结构包括 pci_bus、pci_dev、pci_ops。理解它们之间的关联是调试链路的基础。
| 数据结构 | 作用 | 关键字段 |
|---|---|---|
pci_bus |
代表一条 PCI 总线,包含设备链表与子总线链表 | number, primary, secondary, subordinate, devices, children |
pci_dev |
代表一个 PCI 设备或桥 | devfn, vendor, device, hdr_type, bus |
pci_ops |
配置空间读写方法(直接访问或通过 ACPI/ECAM) | read, write 函数指针 |
4.4 调试与故障排查要点
- 扫描中断: 如果
pci_scan_bus()在某个 devfn 上卡死,通常是因为配置空间读取超时(如设备未响应)。使用pci=nommconf内核参数回退到 IO 端口访问。 - 总线号冲突: 检查
/sys/bus/pci/devices/下的总线号是否连续。若出现跳跃,可能是桥的subordinate bus number设置错误。 - 资源分配失败: 扫描完成后
pci_assign_unassigned_resources()可能因 MMIO 空间不足而失败,导致设备 BAR 未分配地址。查看dmesg | grep pci中的 "no space" 错误。
实战命令:
echo 1 > /sys/bus/pci/rescan 可以触发内核重新扫描总线,无需重启即可发现热插拔设备。该操作会再次调用 pci_scan_bus()。