3. Linux内核PCIe子系统架构:核心数据结构与驱动模型
PCIe子系统是Linux内核中设备模型最成熟的实现之一。理解pci_bus、pci_dev、pci_driver三者之间的关系,以及内核如何枚举PCIe树,是进行PCIe驱动开发和调试的基础。本章将拆解这些核心数据结构,并梳理从硬件复位到驱动绑定的完整流程。
3.1 核心数据结构关系总览
PCIe子系统围绕三个核心结构体构建:pci_bus表示一条PCI总线,pci_dev表示一个PCI设备或桥,pci_driver表示一个PCI驱动程序。它们通过Linux设备模型(device/device_driver)连接,形成树状拓扑。
3.2 数据结构深度解析
3.2.1 struct pci_bus — 总线节点
pci_bus 代表一条PCI/PCIe总线。在PCIe层次结构中,每个总线段(Bus Segment)对应一个 pci_bus 实例。关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
parent | struct pci_bus * | 父总线(上游总线)指针 |
children | struct list_head | 子总线链表头 |
devices | struct list_head | 本总线上的设备链表头 |
number | unsigned char | 本总线的总线号 |
primary | unsigned char | 主总线号(用于桥接) |
secondary | unsigned char | 从属总线号(桥下游起始) |
subordinate | unsigned char | 最大从属总线号 |
bridge | struct device * | 指向桥接设备的 device 结构 |
self | struct pci_dev * | 指向桥接设备自身的 pci_dev |
resource | struct resource * | 总线上的资源(IO/MEM)数组 |
/sys/devices/pci0000:00/ 下可以看到每个 pci_bus 对应的目录。使用 lspci -t 可以查看总线拓扑树。
3.2.2 struct pci_dev — 设备节点
pci_dev 是PCIe子系统中最核心的数据结构,代表一个PCIe设备(包括Endpoint、Switch、Root Port等)。每个 pci_dev 内嵌一个 struct device,从而融入Linux设备模型。
| 字段 | 类型 | 说明 |
|---|---|---|
bus | struct pci_bus * | 设备所在的总线 |
dev | struct device | 内嵌设备模型结构 |
vendor | unsigned short | 厂商ID (VID) |
device | unsigned short | 设备ID (DID) |
subsystem_vendor | unsigned short | 子系统厂商ID |
subsystem_device | unsigned short | 子系统设备ID |
class | unsigned int | 类别代码(Base Class + Sub Class) |
devfn | unsigned int | 设备功能号(Device + Function) |
current_state | pci_power_t | 当前电源状态 |
resource | struct resource[DEVICE_COUNT_RESOURCE] | BAR空间资源数组 |
driver | struct pci_driver * | 绑定的驱动指针 |
3.2.3 struct pci_driver — 驱动节点
pci_driver 是驱动开发者需要实现的结构体。它通过 id_table 声明支持的设备,通过 probe/remove 回调与设备生命周期交互。
// 一个典型的pci_driver定义示例
static struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x9034) }, // Xilinx DMA
{ PCI_DEVICE(0x8086, 0x1533) }, // Intel I210
{ 0, } // 终止条目
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
// 1. 启用设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 2. 请求BAR资源
ret = pci_request_regions(pdev, "my_driver");
if (ret)
goto err_disable;
// 3. 设置DMA掩码
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
goto err_release;
// 4. 读取配置空间关键信息
u16 vendor = pdev->vendor;
u16 device = pdev->device;
u8 rev = pdev->revision;
dev_info(&pdev->dev, "Probed: %04x:%04x rev %02x\n", vendor, device, rev);
return 0;
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
static void my_pci_remove(struct pci_dev *pdev)
{
pci_release_regions(pdev);
pci_disable_device(pdev);
}
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);
3.3 PCIe枚举流程
PCIe枚举是系统启动时由内核自动完成的,核心逻辑在 drivers/pci/probe.c 中。枚举过程本质上是深度优先遍历PCIe树,为每个设备分配总线号、配置空间和资源。
3.3.1 枚举关键步骤
- Root Complex扫描:系统启动时,
pci_scan_root_bus()从总线0开始扫描。对于PCIe,通常由ACPI或设备树提供Root Complex的配置信息。 - 配置空间读取:对每个总线上的每个设备/功能(共32个设备×8个功能),读取配置空间Header(前64字节),判断是否存在设备(Vendor ID != 0xFFFF)。
- pci_dev分配:如果设备存在,分配
pci_dev结构,填充VID、DID、Class Code、Revision等基本信息。 - 类型判断:根据Header Type判断设备类型:
- Type 0:Endpoint设备
- Type 1:PCI/PCIe桥(Bridge)
- Type 2:CardBus桥
- 资源分配:
- Endpoint:读取BAR寄存器,获取设备需要的MMIO/IO空间大小,分配资源。
- Bridge:读取桥的Base/Limit寄存器,分配子总线号,然后递归扫描下游总线。
- 设备注册:调用
pci_bus_add_device()将设备注册到设备模型,触发pci_bus_type的match和probe。
3.4 驱动模型与匹配机制
PCIe驱动模型的核心是 pci_bus_type,它定义了设备与驱动的匹配规则。当一个新的 pci_dev 注册到内核时,设备模型核心会遍历所有已注册的 pci_driver,调用 pci_bus_match() 进行匹配。
3.4.1 匹配规则
| 匹配方式 | 匹配依据 | 优先级 |
|---|---|---|
| VID/DID精确匹配 | PCI_DEVICE(vendor, device) | 最高 |
| VID + Class匹配 | PCI_DEVICE_CLASS(vendor, class_mask) | 中 |
| Class Only匹配 | PCI_CLASS(class_mask) | 低 |
| 通配符匹配 | PCI_ANY_ID | 最低(极少使用) |
// pci_bus_match 核心逻辑(简化)
static int pci_bus_match(struct device *dev, struct device_driver *drv)
{
struct pci_dev *pci_dev = to_pci_dev(dev);
struct pci_driver *pci_drv = to_pci_driver(drv);
const struct pci_device_id *ids = pci_drv->id_table;
if (!ids)
return 0;
// 遍历id_table,调用pci_match_one_device
while (ids->vendor || ids->subvendor || ids->class_mask) {
if (pci_match_one_device(ids, pci_dev))
return 1; // 匹配成功
ids++;
}
return 0; // 无匹配
}
3.4.2 probe回调执行顺序
当匹配成功后,内核按以下顺序调用驱动回调:
- pci_driver.probe():驱动初始化入口,执行设备使能、资源申请、DMA设置等。
- pci_enable_device():通常在probe内部调用,使能I/O和Memory空间访问。
- pci_request_regions():请求BAR资源,防止冲突。
- dma_set_mask():设置DMA地址掩码,确保DMA操作正确。
- pci_set_master():设置设备为Bus Master,允许发起DMA。
probe 没有被调用,请检查:1.
id_table 中的VID/DID是否与设备配置空间一致。2. 设备是否被其他驱动占用(如
pci-stub)。3. 内核是否编译了该驱动(
lsmod | grep driver_name)。4. 使用
lspci -vvv -s 01:00.0 查看 Kernel driver in use 字段。
3.5 实战:查看枚举结果
在调试过程中,可以通过以下方式验证枚举结果:
# 查看PCIe设备树拓扑
lspci -t
# 查看某个设备的详细信息(包括BAR、IRQ、驱动)
lspci -vvv -s 01:00.0
# 查看sysfs中的设备层次
ls -l /sys/devices/pci0000:00/
# 查看驱动绑定的设备
ls -l /sys/bus/pci/drivers/my_pci_driver/
# 手动绑定/解绑(调试时常用)
echo "0000:01:00.0" > /sys/bus/pci/drivers/my_pci_driver/bind
echo "0000:01:00.0" > /sys/bus/pci/drivers/my_pci_driver/unbind
lspci -vvv 可以显示设备的完整配置空间内容,包括Capability链表、MSI-X表、BAR地址等,是PCIe调试的第一利器。
3.6 本章小结
本章深入剖析了PCIe子系统的三大核心数据结构:pci_bus(总线)、pci_dev(设备)、pci_driver(驱动),以及它们通过Linux设备模型形成的树状拓扑关系。枚举流程从Root Complex扫描开始,经过配置空间读取、设备类型判断、资源分配,最终完成设备注册与驱动匹配。理解这些基础架构,是后续进行配置空间解析、BAR映射、DMA调试的前提。