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 枚举流程关键步骤详解
- Host Bridge 初始化:通过 ACPI 或设备树获取配置空间基址,创建根
pci_bus(bus number = 0)。 - 扫描 Slot:对每个设备号(0~31),读取配置空间 Vendor ID(偏移 0x00)。若为 0xFFFF,表示无设备;否则创建
pci_dev。 - 读取配置空间:填充
pci_dev的 vendor、device、class、irq、BAR 等字段。 - 桥设备处理:若设备类型为 PCI-PCI 桥(class code 0x060400),则读取桥的 secondary/subordinate 总线号,创建子
pci_bus,递归扫描。 - 资源分配:所有设备枚举完成后,
pci_assign_unassigned_resources()为未分配 BAR 的设备分配地址空间。 - 设备注册:
pci_bus_add_devices()将pci_dev加入内核设备模型,触发驱动 probe。
热插拔场景差异: 热插拔枚举不经过 ACPI 的静态扫描,而是由
pciehp 或 shpchp 驱动检测到 Presence Detect 状态变化后,调用 pci_scan_slot() 进行动态枚举。此时不会重新分配总线号,而是复用已有拓扑。
4.6 数据结构与枚举流程的SVG全景图
下图展示了 pci_bus 与 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 结构保留,等待下一次插入。