4. Linux PCIe子系统架构
Linux内核的PCIe子系统是连接硬件设备与设备驱动程序的桥梁,其设计核心在于通过三个关键数据结构——pci_bus、pci_dev和pci_driver——来抽象和管理PCIe设备。理解这三者之间的关系以及内核PCI核心层(PCI Core)的工作流程,是进行PCIe设备驱动开发的基础。本章将深入剖析这些数据结构的定义、作用与关联,并梳理从设备枚举到驱动绑定的完整流程,为后续的BAR空间操作实战打下坚实的理论基础。
上图展示了Linux PCIe子系统的整体架构。PCI Core位于顶层,向下管理着pci_bus、pci_dev和pci_driver三个核心数据结构。系统启动时,PCI Core通过枚举扫描PCIe总线,为每个发现的设备创建pci_dev实例,并将其挂载到对应的pci_bus上。当驱动程序调用pci_register_driver注册pci_driver时,PCI Core会执行匹配逻辑,将驱动与设备绑定,最终调用驱动的probe函数完成初始化。下面我们将逐一剖析这些数据结构。
4.1 核心数据结构剖析
4.1.1 struct pci_bus:PCIe总线的抽象
struct pci_bus代表一条PCIe总线,它可以是根总线(Root Bus)或由PCIe桥(Bridge)扩展出的次级总线。该结构体是PCIe拓扑结构的节点,负责维护总线上的设备列表和总线资源。
struct pci_bus {
struct list_head node; /* 全局总线链表节点 */
struct pci_bus *parent; /* 父总线(上游总线) */
struct list_head children; /* 子总线链表(下游总线) */
struct list_head devices; /* 挂接在该总线上的所有设备链表 */
struct pci_dev *self; /* 指向创建此总线的PCIe桥设备 */
struct resource *resource[PCI_BRIDGE_RESOURCE_NUM]; /* 总线窗口资源 */
struct pci_ops *ops; /* 配置空间访问操作集 */
unsigned char number; /* 总线编号 */
unsigned char primary; /* 主总线号(上游) */
unsigned char secondary; /* 次级总线号(本总线) */
unsigned char subordinate; /* 下游最大总线号 */
...
};
在驱动开发中,我们通常不直接操作pci_bus,但理解其作用至关重要。例如,当需要遍历某条总线上的所有设备时,可以通过list_for_each_entry遍历devices链表。总线资源(resource数组)定义了该总线可以访问的MMIO和I/O端口范围,这直接影响了BAR空间的分配。
4.1.2 struct pci_dev:PCIe设备的软件表示
struct pci_dev是内核中表示一个PCIe设备的核心结构体,它包含了设备的配置空间信息、资源信息以及运行时状态。每个物理PCIe设备(或PCIe桥)在枚举时都会创建一个对应的pci_dev实例。
struct pci_dev {
struct list_head bus_list; /* 总线上的设备链表节点 */
struct pci_bus *bus; /* 指向所在总线 */
struct pci_bus *subordinate; /* 若为桥,指向下游总线 */
unsigned int devfn; /* 设备号+功能号 */
unsigned short vendor; /* 厂商ID */
unsigned short device; /* 设备ID */
unsigned short subsystem_vendor;/* 子系统厂商ID */
unsigned short subsystem_device;/* 子系统设备ID */
unsigned int class; /* 类别代码 */
struct pci_slot *slot; /* 插槽信息 */
struct resource resource[DEVICE_COUNT_RESOURCE]; /* BAR资源数组 */
struct pci_driver *driver; /* 绑定的驱动 */
...
unsigned int irq; /* 中断号 */
...
};
在BAR空间操作中,resource数组是最关键的字段。resource[0]到resource[5]对应BAR0到BAR5,每个resource结构体包含start(物理地址)、end(结束地址)、flags(类型标志)等信息。驱动通过访问这些资源来映射BAR空间。
4.1.3 struct pci_driver:驱动的注册与匹配
struct pci_driver是PCIe设备驱动的核心接口,它定义了驱动与内核PCI Core交互的协议。驱动开发者需要填充该结构体并调用pci_register_driver完成注册。
struct pci_driver {
struct list_head node; /* 驱动链表节点 */
const char *name; /* 驱动名称 */
const struct pci_device_id *id_table; /* 支持的设备ID表 */
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);
...
};
id_table是驱动与设备匹配的关键,它是一个pci_device_id数组,定义了该驱动支持的Vendor ID、Device ID组合。PCI Core在注册驱动时会遍历系统中的所有pci_dev,通过pci_match_id函数进行匹配。匹配成功后,PCI Core会调用驱动的probe函数,传入对应的pci_dev指针和匹配的ID条目。
4.2 PCI核心层工作流程
4.2.1 设备枚举与扫描
系统启动时,PCI Core通过pci_scan_bus开始对PCIe总线进行深度优先遍历。该过程的核心步骤如下:
- 总线扫描:从总线0开始,读取每个总线槽位上设备的配置空间头部(Vendor ID和Device ID)。若读取到0xFFFFFFFF,表示该槽位无设备。
- 设备创建:为每个发现的设备分配
pci_dev结构体,读取完整的配置空间信息(包括BAR、中断线、能力链表等)。 - 桥设备处理:若设备是PCIe桥(Class Code为0x060400),则为其分配次级总线号,并递归扫描下游总线。
- 资源分配:扫描完成后,PCI Core根据设备的BAR请求大小和总线窗口,为每个BAR分配物理地址空间。
- 设备注册:调用
pci_bus_add_device将设备添加到内核设备模型,此时设备会出现在/sys/bus/pci/devices/目录下。
关键点:BAR空间的地址分配发生在设备枚举阶段,由PCI Core统一完成。驱动开发者通常不需要关心BAR地址的具体值,只需通过pci_resource_start()等API获取即可。
4.2.2 驱动注册与匹配
驱动模块加载时,调用pci_register_driver将pci_driver注册到PCI Core。PCI Core执行以下流程:
- 驱动链表插入:将驱动添加到全局驱动链表
pci_drivers中。 - 设备遍历:遍历系统中所有已注册的
pci_dev,调用pci_bus_match进行匹配。 - ID匹配:
pci_bus_match调用pci_match_id,将设备的Vendor ID、Device ID、Subsystem ID等与驱动的id_table逐项比较。 - 驱动绑定:匹配成功后,调用
pci_device_probe,最终触发驱动的probe函数。
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x9038) }, /* Xilinx PCIe设备 */
{ 0, }
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
static int my_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
/* 初始化硬件,映射BAR空间 */
return 0;
}
static struct pci_driver my_pci_driver = {
.name = "my_pci_driver",
.id_table = my_pci_ids,
.probe = my_pci_probe,
.remove = my_pci_remove,
};
module_pci_driver(my_pci_driver);
4.3 实战案例:从枚举到驱动的完整链路
假设我们有一块基于Xilinx Kintex-7 FPGA的PCIe开发板,FPGA内部实现了自定义的DMA控制器,Vendor ID为0x10EE,Device ID为0x9038。系统启动后,我们通过lspci命令查看设备状态:
# lspci -v -s 01:00.0
01:00.0 Memory controller: Xilinx Corporation Device 9038
Subsystem: Xilinx Corporation Device 0007
Flags: bus master, fast devsel, latency 0, IRQ 16
Memory at fe800000 (64-bit, non-prefetchable) [size=1M]
Memory at fe900000 (64-bit, non-prefetchable) [size=64K]
Capabilities: [40] Power Management version 3
Capabilities: [50] MSI: Enable- Count=1/1 Maskable- 64bit+
Capabilities: [70] Express Endpoint, MSI 00
从输出可以看到,该设备有两个BAR区域:BAR0大小为1MB(用于DMA描述符),BAR1大小为64KB(用于控制寄存器)。IRQ分配为16(传统中断),MSI功能已列出但未启用。
接下来,我们编写驱动并注册:
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
resource_size_t bar0_start, bar0_len;
void __iomem *bar0_virt;
/* 1. 启用PCIe设备 */
ret = pci_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "Failed to enable PCIe device\n");
return ret;
}
/* 2. 请求BAR0资源 */
ret = pci_request_regions(pdev, "my_pci_driver");
if (ret) {
dev_err(&pdev->dev, "Failed to request regions\n");
goto err_disable;
}
/* 3. 获取BAR0物理地址和长度 */
bar0_start = pci_resource_start(pdev, 0);
bar0_len = pci_resource_len(pdev, 0);
dev_info(&pdev->dev, "BAR0: start=0x%llx len=0x%llx\n",
(unsigned long long)bar0_start, (unsigned long long)bar0_len);
/* 4. 映射BAR0到内核虚拟地址空间 */
bar0_virt = pci_iomap(pdev, 0, bar0_len);
if (!bar0_virt) {
dev_err(&pdev->dev, "Failed to iomap BAR0\n");
goto err_release;
}
/* 5. 读取设备版本寄存器(假设偏移0x00) */
u32 version = ioread32(bar0_virt + 0x00);
dev_info(&pdev->dev, "Device version: 0x%08x\n", version);
/* 保存映射地址供后续使用 */
pci_set_drvdata(pdev, bar0_virt);
return 0;
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
在上述代码中,我们遵循了标准的PCIe驱动初始化流程:pci_enable_device使能设备(包括开启总线主控、I/O和内存空间),pci_request_regions申请BAR资源以避免冲突,pci_iomap将物理地址映射为内核虚拟地址。通过ioread32读取寄存器验证映射是否正确。
经验提示:在probe函数中,务必检查每一步的返回值。特别是pci_enable_device失败时,通常是因为设备已被其他驱动占用或电源管理状态异常。此外,pci_request_regions失败往往意味着BAR地址冲突,需要检查BIOS分配的资源是否合理。
4.4 常见误区与总结复盘
| 误区/问题 | 原因分析 | 正确做法 |
|---|---|---|
直接使用ioremap映射BAR地址 |
未通过pci_request_regions申请资源,可能导致地址冲突 |
先调用pci_request_regions,再使用pci_iomap |
忽略pci_enable_device的返回值 |
设备可能处于D3状态或电源管理异常 | 检查返回值,失败时打印错误并退出probe |
| 在remove函数中未释放映射和资源 | 导致内存泄漏和资源占用 | 按顺序调用pci_iounmap、pci_release_regions、pci_disable_device |
| 混淆BAR的物理地址与虚拟地址 | BAR空间是物理地址,CPU无法直接访问 | 必须通过pci_iomap或ioremap转换为虚拟地址 |
| 未检查BAR类型(I/O或内存) | I/O BAR和内存BAR的访问方式不同 | 通过pci_resource_flags检查IORESOURCE_IO或IORESOURCE_MEM标志 |
总结本章内容,理解pci_bus、pci_dev和pci_driver是掌握Linux PCIe驱动开发的关键。PCI Core层通过枚举创建设备树,通过匹配机制将驱动与设备绑定,最终通过probe函数让驱动获得设备的控制权。在实际开发中,开发者应重点关注pci_dev中的resource数组,这是访问BAR空间的入口。同时,遵循标准的初始化流程(enable → request_regions → iomap)和清理流程(iounmap → release_regions → disable)是保证驱动稳定性的基础。下一章我们将深入探讨BAR空间的详细操作技术,包括如何解析BAR类型、处理64位BAR以及动态配置BAR地址。