3. Linux PCIe 子系统架构:pci_bus、pci_dev、pci_driver 数据结构与核心 API

Linux PCIe 子系统是内核中最为成熟的设备模型之一,它通过三层核心数据结构——pci_bus、pci_dev、pci_driver——将物理总线拓扑、功能设备与驱动逻辑解耦。理解这三者的关系与核心 API,是编写正确 PCIe 驱动的基石。

3.1 核心数据结构层次

PCIe 子系统采用“总线 → 设备 → 驱动”的三层模型,与 Linux 设备模型(bus_type、device、device_driver)一一对应。下图展示了这三者在系统中的组织关系:

PCIe 子系统三层数据结构关系 pci_bus struct pci_bus pci_dev struct pci_dev pci_driver struct pci_driver 包含 绑定 关键属性 • pci_bus: parent(父总线)、children(子总线链表)、devices(设备链表)、 ops(总线操作)、resource(总线资源) • pci_dev: bus(所属总线)、devfn(设备功能号)、vendor/device(厂商/设备ID)、 irq(中断号)、resource[](6个BAR资源)、driver(绑定的驱动) • pci_driver: name(驱动名称)、id_table(支持的设备ID表)、 probe/remove(探测/移除回调)、suspend/resume(电源管理回调) • 绑定机制: pci_match_device() 遍历 id_table,匹配 vendor/device/class 后调用 probe()

3.2 pci_bus:总线拓扑的骨架

struct pci_bus 是 PCIe 层次化拓扑的核心表示。每个 PCIe 域(Domain)对应一个根总线(Root Bus),其下通过桥设备(Bridge)挂载子总线。关键字段如下:

字段类型说明
parentstruct pci_bus *父总线指针,根总线为 NULL
childrenstruct list_head子总线链表头
devicesstruct list_head挂载在该总线上的设备链表
numberunsigned char总线编号(Bus Number)
primaryunsigned char桥上游总线号
secondaryunsigned char桥下游总线号
resource[]struct resource *总线地址窗口(MEM/IO)
opsstruct pci_ops *底层配置空间访问方法
开发提示: 在驱动中获取当前设备所在总线时,使用 dev->bus 即可。遍历子总线或兄弟设备时,需操作 bus->children 或 bus->devices 链表,并加锁保护。

3.3 pci_dev:功能设备的抽象

struct pci_dev 代表一个 PCIe 功能(Function),无论是物理设备还是虚拟功能(VF)。驱动开发者最常打交道的字段包括:

struct pci_dev {
    struct pci_bus  *bus;           // 所属总线
    unsigned int    devfn;          // 设备号(高5位) + 功能号(低3位)
    unsigned short  vendor;         // 厂商ID (0x8086 Intel, 0x10ee Xilinx)
    unsigned short  device;         // 设备ID
    unsigned short  subsystem_vendor; // 子系统厂商ID
    unsigned short  subsystem_device; // 子系统设备ID
    unsigned int    class;          // 类代码 (存储类、网络类等)
    u8              revision;       // 修订版本
    u8              pcie_cap;       // PCIe Capability 偏移
    int             irq;            // 中断号 (INTx/MSI/MSI-X)
    struct resource resource[DEVICE_COUNT_RESOURCE]; // BAR0~BAR5 + ROM
    struct pci_driver *driver;      // 当前绑定的驱动
    unsigned int    is_virtfn:1;    // 是否为SR-IOV虚拟功能
    unsigned int    is_physfn:1;    // 是否为SR-IOV物理功能
};
关键点: devfn 字段编码了设备号和功能号,可通过 PCI_SLOT(devfn) 和 PCI_FUNC(devfn) 宏提取。BAR 资源通过 pci_resource_start() / pci_resource_len() 访问,不要直接读取 resource[] 原始值。

3.4 pci_driver:驱动注册与回调

struct pci_driver 是驱动开发者需要定义和注册的核心结构。其生命周期由 pci_register_driver() 和 pci_unregister_driver() 管理。

struct pci_driver {
    const char *name;                       // 驱动名称,在 /sys/bus/pci/drivers/ 下可见
    const struct pci_device_id *id_table;   // 支持的设备ID表,以 {0,} 结尾
    int  (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
    void (*remove)(struct pci_dev *dev);
    int  (*suspend)(struct pci_dev *dev, pm_message_t state);
    int  (*resume)(struct pci_dev *dev);
    struct device_driver driver;            // 内嵌通用设备驱动
};

设备 ID 表使用 PCI_VDEVICE() 或 { PCI_DEVICE(vendor, device) } 宏定义:

static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x10ee, 0x7021) },  // Xilinx DMA
    { PCI_DEVICE(0x8086, 0x10fb) },  // Intel 82599
    { 0, }  // 终止项
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

3.5 核心 API 速查表

以下 API 是 PCIe 驱动开发中最常用的函数,按功能分类列出:

类别函数说明
注册/注销pci_register_driver()注册 pci_driver 到 PCI 总线
pci_unregister_driver()注销驱动并调用所有 remove()
资源访问pci_resource_start()获取 BAR 起始物理地址
pci_resource_len()获取 BAR 长度
pci_iomap()将 BAR 映射到内核虚拟地址
配置空间pci_read_config_byte/word/dword()读取配置空间寄存器
pci_write_config_byte/word/dword()写入配置空间寄存器
pci_find_capability()查找 Capability 结构偏移
DMApci_set_dma_mask()设置 DMA 地址掩码(32/64位)
pci_alloc_consistent()分配一致性 DMA 缓冲区
中断pci_alloc_irq_vectors()分配 MSI/MSI-X 中断向量
pci_free_irq_vectors()释放中断向量
电源管理pci_save_state()保存设备配置空间状态
pci_restore_state()恢复设备配置空间状态

3.6 驱动探测流程

当 PCI 核心检测到新设备时,触发以下完整流程:

  1. 设备枚举: PCI 核心读取配置空间 Vendor/Device ID,创建 struct pci_dev 并挂入 bus->devices 链表。
  2. 驱动匹配: 遍历已注册的 pci_driver,调用 pci_match_device() 比对 id_table。
  3. probe() 调用: 匹配成功后,调用驱动的 probe() 回调,传入 pci_dev 和匹配的 pci_device_id。
  4. 驱动初始化: 在 probe() 中完成:启用设备(pci_enable_device())、设置 DMA 掩码、映射 BAR、注册中断。
  5. 设备移除: 驱动卸载或设备热拔出时,调用 remove() 释放资源。
PCIe 驱动探测与绑定流程 设备枚举 驱动匹配 probe() 调用 启用设备 DMA 掩码设置 BAR 映射 中断注册 probe() 返回 0
常见错误: 在 probe() 中忘记调用 pci_enable_device() 会导致 BAR 无法访问;忘记设置 DMA 掩码会导致 pci_alloc_consistent() 返回错误地址。建议在 probe() 开头按顺序执行:pci_enable_device() → pci_set_master() → pci_set_dma_mask()。

3.7 设备树与 ACPI 下的差异

虽然 pci_bus/pci_dev/pci_driver 是平台无关的抽象,但底层资源获取方式因固件接口而异:

方面设备树 (DT)ACPI
总线枚举由 pci-host-generic 驱动解析 ranges 属性由 ACPI 命名空间中的 _SB 对象描述
中断映射通过 interrupt-map 属性通过 _PRT (PCI Routing Table) 方法
资源分配静态在 DT 中描述由 BIOS/UEFI 在启动时分配
电源管理依赖 power-domains 属性通过 _PS0/_PS3 方法控制
驱动匹配兼容 of_match_table 与 pci_device_id仅使用 pci_device_id,ACPI ID 通过 acpi_match_device() 辅助
跨平台策略: 编写通用驱动时,建议同时提供 pci_device_id 和 of_match_table。在 probe() 中通过 dev->dev.of_node 判断是否运行在设备树平台,从而选择不同的初始化路径。