4. Linux PCI子系统架构:pci_bus、pci_dev数据结构与设备枚举流程

Linux PCI子系统是操作系统与PCI/PCIe硬件之间的核心桥梁。理解其核心数据结构pci_bus与pci_dev,以及设备枚举(Enumeration)流程,是掌握热插拔与AER错误处理集成的基础。本章将深入剖析这两个关键数据结构的设计意图与关联,并完整呈现从系统启动到设备发现的枚举链路。

4.1 核心数据结构:pci_bus 与 pci_dev

Linux内核使用两个核心结构体来抽象PCI/PCIe总线拓扑:struct pci_bus 表示一条逻辑PCI总线,struct pci_dev 表示一个PCI功能(Function)。两者通过树形链表组织,形成完整的拓扑视图。

数据结构 内核头文件 核心字段 职责
struct pci_bus include/linux/pci.h number, parent, children, devices, ops, bridge 代表一条PCI总线,管理挂载其上的设备链表,提供配置空间访问方法
struct pci_dev include/linux/pci.h bus, devfn, vendor, device, class, irq, resource[], driver 代表一个PCI功能,包含配置空间信息、资源分配、电源状态与驱动绑定
关键设计原则: pci_bus 通过 bridge 字段指向其上游桥设备(pci_dev),而 pci_dev 通过 bus 字段指向其所在总线。这种双向引用构成了递归遍历的拓扑基础。

4.2 pci_bus 结构体深度解析

struct pci_bus 是总线拓扑的节点。其关键字段如下:

  • number / primary / secondary / subordinate:总线号范围,用于配置空间路由。
  • parent / children / node:构成总线树形结构,parent 指向上游总线,children 链表挂载下游子总线。
  • devices:链表头,挂载该总线上所有 pci_dev 实例。
  • ops:struct pci_ops *,指向配置空间访问方法(如 pci_conf1_read / pci_conf1_write)。
  • bridge:指向生成该总线的桥设备(pci_dev),根总线此字段为 NULL。
  • self:指向桥设备自身的 pci_dev(与 bridge 类似,但用于特定场景)。
  • resource[]:总线级别的资源(IO、MEM、BUS 号)窗口。

4.3 pci_dev 结构体深度解析

struct pci_dev 是驱动开发者最常打交道的结构体。它完整描述了一个PCI功能:

字段类别 示例字段 说明
拓扑定位 bus, devfn, slot 总线号+设备号+功能号,唯一标识一个功能
身份标识 vendor, device, subsystem_vendor, subsystem_device, class, revision 从配置空间读取,用于驱动匹配
资源管理 resource[DEVICE_COUNT_RESOURCE] 最多 12 个 BAR 资源,包含起始地址、大小、标志
中断 irq, msi_enabled, msix_enabled INTx 中断号及 MSI/MSI-X 使能状态
电源管理 current_state, pm_cap 当前电源状态(D0/D1/D2/D3hot/D3cold)
驱动绑定 driver, dev 指向已绑定的 pci_driver 及内核通用设备结构
错误状态 error_state pci_channel_state_t,用于 AER 错误恢复流程

4.4 设备枚举流程:从 ACPI 到 pci_scan_bus

Linux PCI 设备枚举发生在系统启动阶段,核心入口是 pci_scan_bus 系列函数。枚举流程遵循深度优先递归策略,从根总线(Host Bridge)开始,逐级扫描下游设备与桥。

枚举触发点: x86 平台通过 ACPI 表(MCFG)发现 PCIe 配置空间基址;ARM64 平台通过设备树或 UEFI 获取 ECAM 区域。统一入口为 pci_host_bridge_probe()。
// 枚举核心调用链(简化)
pci_host_bridge_probe()
  └─> pci_scan_root_bus_bridge()
       └─> pci_scan_child_bus()
            ├─> pci_scan_slot()          // 扫描一个设备号下的所有功能
            │    └─> pci_scan_device()   // 读取 Vendor/Device ID,创建 pci_dev
            │         └─> pci_alloc_dev() // 分配 pci_dev 结构体
            │         └─> pci_setup_device() // 读取配置空间头信息
            ├─> pci_scan_bridge()        // 若发现 PCI-PCI 桥,递归扫描下游总线
            │    └─> pci_scan_child_bus() // 递归调用
            └─> pci_bus_add_devices()    // 将枚举完成的设备注册到内核设备模型
                 └─> device_add()        // 触发 driver 匹配与 probe

4.5 枚举流程关键步骤详解

  1. Host Bridge 初始化:通过 ACPI 或设备树获取配置空间基址,创建根 pci_bus(bus number = 0)。
  2. 扫描 Slot:对每个设备号(0~31),读取配置空间 Vendor ID(偏移 0x00)。若为 0xFFFF,表示无设备;否则创建 pci_dev。
  3. 读取配置空间:填充 pci_dev 的 vendor、device、class、irq、BAR 等字段。
  4. 桥设备处理:若设备类型为 PCI-PCI 桥(class code 0x060400),则读取桥的 secondary/subordinate 总线号,创建子 pci_bus,递归扫描。
  5. 资源分配:所有设备枚举完成后,pci_assign_unassigned_resources() 为未分配 BAR 的设备分配地址空间。
  6. 设备注册:pci_bus_add_devices() 将 pci_dev 加入内核设备模型,触发驱动 probe。
热插拔场景差异: 热插拔枚举不经过 ACPI 的静态扫描,而是由 pciehp 或 shpchp 驱动检测到 Presence Detect 状态变化后,调用 pci_scan_slot() 进行动态枚举。此时不会重新分配总线号,而是复用已有拓扑。

4.6 数据结构与枚举流程的SVG全景图

下图展示了 pci_bus 与 pci_dev 的树形组织关系,以及枚举过程中数据结构的创建顺序。

PCI 总线拓扑与枚举数据结构关系图 pci_bus 0 (Root) number=0, parent=NULL bridge=NULL pci_dev: Host Bridge devfn=00.0, class=0x060000 pci_dev: PCIe EP devfn=01.0, vendor=0x8086 pci_bus 1 (Secondary) number=1, parent=bus0 bridge=PCIe Switch pci_dev: PCIe Switch devfn=00.0, class=0x060400 pci_dev: NVMe SSD devfn=00.0, class=0x010802 pci_bus 2 (Tertiary) number=2, parent=bus1 bridge=Switch Downstream pci_dev: GPU devfn=00.0, class=0x030000 children children 枚举顺序说明: 1. 创建 pci_bus 0 → 扫描 slot 0,创建 Host Bridge pci_dev 2. 扫描 slot 1,发现 PCIe Switch(桥)→ 创建 pci_bus 1,递归扫描 3. 在 bus 1 上发现 NVMe 和下游桥 → 创建 pci_bus 2,继续递归 pci_bus pci_dev (桥) pci_dev (EP) pci_dev (其他)

4.7 枚举流程中的关键函数与调用时机

函数名 调用时机 核心行为
pci_scan_bus() 根总线创建 分配 pci_bus,调用 pci_scan_child_bus()
pci_scan_child_bus() 每条总线扫描入口 遍历设备号 0~31,调用 pci_scan_slot()
pci_scan_slot() 每个设备号 遍历功能号 0~7,调用 pci_scan_device()
pci_scan_device() 每个功能 读取 Vendor/Device ID,分配 pci_dev
pci_setup_device() 创建 pci_dev 后 读取配置空间头类型、BAR、IRQ 等信息
pci_scan_bridge() 发现桥设备 读取桥的总线号寄存器,创建子 pci_bus
pci_bus_add_devices() 整棵树枚举完成后 将 pci_dev 注册到内核设备模型

4.8 热插拔枚举与冷启动枚举的差异

热插拔场景下的枚举并非从头开始,而是基于已有拓扑的增量操作:

  • 冷启动枚举:由 pci_scan_root_bus_bridge() 触发,递归扫描整个树,分配所有总线号与资源。
  • 热插拔枚举:由 pciehp 中断或轮询触发,调用 pci_scan_slot() 仅扫描新增设备号,不重新分配总线号。
  • 资源分配:热插拔时,pci_assign_unassigned_resources() 仅处理新设备的 BAR,不扰动已有设备。
  • 驱动绑定:热插拔枚举后,pci_bus_add_devices() 触发 device_add(),自动完成驱动 probe。
数据结构复用: 热插拔不会销毁或重建 pci_bus 结构。当设备拔出时,pci_stop_and_remove_bus_device() 负责清理 pci_dev 及其子设备,但 pci_bus 结构保留,等待下一次插入。

4.9 小结