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)一一对应。下图展示了这三者在系统中的组织关系:
3.2 pci_bus:总线拓扑的骨架
struct pci_bus 是 PCIe 层次化拓扑的核心表示。每个 PCIe 域(Domain)对应一个根总线(Root Bus),其下通过桥设备(Bridge)挂载子总线。关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
parent | struct pci_bus * | 父总线指针,根总线为 NULL |
children | struct list_head | 子总线链表头 |
devices | struct list_head | 挂载在该总线上的设备链表 |
number | unsigned char | 总线编号(Bus Number) |
primary | unsigned char | 桥上游总线号 |
secondary | unsigned char | 桥下游总线号 |
resource[] | struct resource * | 总线地址窗口(MEM/IO) |
ops | struct 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 结构偏移 | |
| DMA | pci_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 核心检测到新设备时,触发以下完整流程:
- 设备枚举: PCI 核心读取配置空间 Vendor/Device ID,创建
struct pci_dev并挂入bus->devices链表。 - 驱动匹配: 遍历已注册的
pci_driver,调用pci_match_device()比对id_table。 - probe() 调用: 匹配成功后,调用驱动的
probe()回调,传入pci_dev和匹配的pci_device_id。 - 驱动初始化: 在
probe()中完成:启用设备(pci_enable_device())、设置 DMA 掩码、映射 BAR、注册中断。 - 设备移除: 驱动卸载或设备热拔出时,调用
remove()释放资源。
常见错误: 在 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 判断是否运行在设备树平台,从而选择不同的初始化路径。