3. Linux 内核 PCI 子系统初始化流程:pci_init()、pci_scan_bus()、pci_assign_unassigned_resources()

在 Linux 内核启动过程中,PCI 子系统的初始化是设备枚举与资源分配的核心环节。整个流程可划分为三个阶段:子系统框架初始化(pci_init())、总线拓扑扫描(pci_scan_bus())以及未分配资源重排(pci_assign_unassigned_resources())。这三个函数分别对应 PCI 枚举的“准备—发现—分配”三部曲。

关键认知: PCI 子系统的初始化并非一次性完成,而是分阶段、分层次地推进。理解每个阶段的职责与依赖关系,是分析多网卡资源冲突与 ACPI 中断路由的基础。

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 扫描流程

  1. 根总线创建: 调用 pci_create_root_bus() 创建总线 0,并绑定 ACPI 句柄。
  2. 设备扫描: 遍历总线 0 上的所有设备号(0~31),读取 Vendor ID 寄存器,若为 0xFFFF 则跳过。
  3. 桥设备处理: 若设备类型为 PCI-PCI 桥,则递归调用 pci_scan_bridge() 扫描次级总线。
  4. 多功能设备: 对支持多功能的设备,扫描 Function 0~7。
  5. 分配总线号: 为每个桥分配次级总线号(Secondary Bus Number)与 subordinate 总线号。
性能数据: 在包含 4 个 PCIe 交换器、8 个网卡的系统上,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 分配流程

  1. 收集未分配设备: 遍历所有 pci_dev,检查 resource 数组中的 flags 是否包含 IORESOURCE_UNSET。
  2. 桥窗口计算: 对每个 PCI-PCI 桥,计算其下所有设备所需资源的总和,并考虑对齐与桥本身的限制。
  3. 递归分配: 从叶节点(无桥的设备)开始分配,逐级向上更新桥窗口的基地址与限制寄存器。
  4. 写入配置空间: 将计算出的基地址写入 BAR 寄存器,并验证写入回读是否一致。
常见陷阱: 当多个网卡共享同一个 PCI-PCI 桥时,若桥的 I/O 窗口大小不足(通常为 4KB),可能导致部分网卡的 I/O 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() 之间的调用关系与数据流:

PCI 子系统初始化三阶段协作流程 阶段 1: pci_init() 配置空间访问方法注册 ACPI-PCI 绑定 资源树根节点初始化 阶段 2: pci_scan_bus() 递归遍历总线拓扑 发现 PCI 设备与桥 分配总线号 阶段 3: pci_assign_ unassigned_resources() 收集未分配设备 桥窗口计算与递归分配 提供 ACPI 句柄 传递设备列表 输出数据结构 pci_bus 树(总线拓扑) + pci_dev 链表(设备列表) + resource 树(I/O/MEM 分配结果) ACPI _PRT 中断路由表与 pci_dev->irq 绑定 多网卡场景下的关键依赖 • pci_init() 必须成功绑定 ACPI,否则 _PRT 不可用 → 中断路由失效 • pci_scan_bus() 必须正确识别桥设备,否则次级总线设备被遗漏 • pci_assign_unassigned_resources() 需在桥窗口内分配,否则网卡 BAR 冲突

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,强制内核重新分配所有总线号与资源,避免固件预留不足导致的分配失败。