3. Linux 内核 PCI 子系统初始化流程:pci_init()、pci_scan_bus()、pci_assign_unassigned_resources()
在 Linux 内核启动过程中,PCI 子系统的初始化是设备枚举与资源分配的核心环节。整个流程可划分为三个阶段:子系统框架初始化(pci_init())、总线拓扑扫描(pci_scan_bus())以及未分配资源重排(pci_assign_unassigned_resources())。这三个函数分别对应 PCI 枚举的“准备—发现—分配”三部曲。
3.1 pci_init():PCI 子系统框架初始化
pci_init() 是 PCI 子系统的入口函数,在 arch_initcall(pci_init) 宏的驱动下,于内核启动早期被调用。其主要职责是建立 PCI 子系统运行所需的基础设施,包括数据结构、配置空间访问方法以及 ACPI 与 PCI 的绑定。
3.1.1 核心任务清单
| 任务 | 调用函数/机制 | 说明 |
|---|---|---|
| 初始化 PCI 总线锁 | pci_lock 自旋锁 |
保护全局 PCI 数据结构并发访问 |
| 注册 PCI 配置空间访问方法 | pci_direct_init() / pci_mmcfg_init() |
选择 PCI Configuration Mechanism #1/#2 或 MMCONFIG |
| 建立 ACPI-PCI 绑定 | acpi_pci_init() |
将 ACPI 命名空间中的 PCI 设备对象与 PCI 子系统关联 |
| 注册 PCI 总线类型 | bus_register(&pci_bus_type) |
使能 PCI 总线的设备模型支持(uevent、电源管理) |
| 初始化 PCI 资源管理 | pci_resource_init() |
建立 I/O 与 MEM 资源树根节点 |
3.1.2 ACPI-PCI 绑定的关键作用
在 pci_init() 阶段,acpi_pci_init() 会扫描 ACPI 命名空间中的 \_SB_.PCI0 等设备对象,并建立 acpi_device 与 pci_dev 之间的映射关系。这一绑定是后续中断路由查询(acpi_pci_irq_lookup)和资源分配(_CRS 解析)的前提。
pci_init() 阶段 ACPI 绑定失败,后续 pci_scan_bus() 将无法获取 _PRT 中断路由表,导致所有 PCI 设备使用相同的默认中断号,极易引发中断冲突。
3.2 pci_scan_bus():总线拓扑扫描与设备发现
pci_scan_bus() 是 PCI 枚举的核心函数,负责递归遍历 PCI 总线拓扑,发现所有存在的 PCI 设备与桥,并为其分配 pci_dev 结构体。该函数通常由 pcibios_init() 或 ACPI 命名空间中的 \_SB_.PCI0 扫描触发。
3.2.1 扫描流程
- 根总线创建: 调用
pci_create_root_bus()创建总线 0,并绑定 ACPI 句柄。 - 设备扫描: 遍历总线 0 上的所有设备号(0~31),读取 Vendor ID 寄存器,若为
0xFFFF则跳过。 - 桥设备处理: 若设备类型为 PCI-PCI 桥,则递归调用
pci_scan_bridge()扫描次级总线。 - 多功能设备: 对支持多功能的设备,扫描 Function 0~7。
- 分配总线号: 为每个桥分配次级总线号(Secondary Bus Number)与 subordinate 总线号。
pci_scan_bus() 平均执行 1200 次配置空间读取,耗时约 15ms(基于 MMCONFIG 访问)。
3.2.2 扫描结果的数据结构
扫描完成后,内核建立如下层级关系:
pci_bus 0 (Root Bus)
├── pci_dev 00:00.0 (Host Bridge)
├── pci_dev 00:01.0 (PCI-PCI Bridge) → pci_bus 1
│ ├── pci_dev 01:00.0 (Network Card #1)
│ └── pci_dev 01:00.1 (Network Card #2)
├── pci_dev 00:02.0 (PCI-PCI Bridge) → pci_bus 2
│ └── pci_dev 02:00.0 (Network Card #3)
└── pci_dev 00:1c.0 (PCIe Root Port) → pci_bus 3
└── pci_dev 03:00.0 (Network Card #4)
3.3 pci_assign_unassigned_resources():未分配资源重排
在扫描完成后,部分设备可能因固件预留不足或热插拔等原因,未获得有效的 I/O 或 MEM 地址范围。pci_assign_unassigned_resources() 负责对这些“孤儿”设备进行资源重排,确保所有设备都能被操作系统访问。
3.3.1 资源分配策略
| 资源类型 | 分配算法 | 对齐要求 | 典型大小 |
|---|---|---|---|
| I/O 空间 | 从 I/O 资源树底部向上分配 | 4 字节对齐 | 256 字节(标准网卡) |
| 32-bit MEM | 从 MEM 资源树底部向上分配 | 1MB 对齐(大 BAR) | 16MB~256MB(网卡 MMIO) |
| 64-bit MEM | 从 64-bit 窗口底部向上分配 | 1MB 对齐 | 1GB+(高性能网卡) |
| PCIe 扩展 ROM | 在桥窗口内分配 | 4KB 对齐 | 64KB~1MB |
3.3.2 分配流程
- 收集未分配设备: 遍历所有
pci_dev,检查resource数组中的flags是否包含IORESOURCE_UNSET。 - 桥窗口计算: 对每个 PCI-PCI 桥,计算其下所有设备所需资源的总和,并考虑对齐与桥本身的限制。
- 递归分配: 从叶节点(无桥的设备)开始分配,逐级向上更新桥窗口的基地址与限制寄存器。
- 写入配置空间: 将计算出的基地址写入 BAR 寄存器,并验证写入回读是否一致。
pci 0000:01:00.1: BAR 0: no space for [io size 0x100] 错误。
3.4 三阶段协作流程总览
下图展示了 pci_init()、pci_scan_bus() 与 pci_assign_unassigned_resources() 之间的调用关系与数据流:
3.5 多网卡场景下的资源分配挑战
当系统中存在多张网卡(尤其是不同厂商、不同 BAR 大小的网卡)时,pci_assign_unassigned_resources() 的分配顺序与对齐策略直接影响资源利用率:
- I/O 空间碎片化: 网卡 I/O BAR 通常为 256 字节,但桥窗口以 4KB 对齐,导致大量 I/O 空间浪费。
- 64-bit MEM 窗口不足: 高端网卡(如 Mellanox ConnectX-6)需要 1GB 以上的 64-bit MEM 空间,若桥窗口未预留给 64-bit 地址,分配将失败。
- 热插拔预留: 部分固件在
_CRS中预留了热插拔空间,但pci_assign_unassigned_resources()可能忽略这些预留,导致后续热插拔设备无资源可用。
pci=assign-busses 和 pci=realloc,强制内核重新分配所有总线号与资源,避免固件预留不足导致的分配失败。