2. Linux PCIe 子系统架构:pci_bus/pci_dev/pci_driver 数据结构与注册流程
Linux PCIe 子系统是内核中负责管理 PCI/PCIe 设备的核心框架。它通过三个核心数据结构——pci_bus、pci_dev 和 pci_driver——来抽象总线拓扑、设备实例与驱动逻辑。理解这三者的关系与注册流程,是调试 PCIe 驱动崩溃、设备枚举失败、资源分配冲突等问题的前提。
2.1 核心数据结构概览
下表对比了三个核心数据结构的职责、生命周期与关键字段:
| 数据结构 | 职责 | 生命周期 | 关键字段(调试关注) |
|---|---|---|---|
pci_bus |
表示一条 PCI 总线,管理挂载其上的设备与子总线 | 总线扫描时创建,系统运行期间常驻 | number(总线号)、self(指向桥设备)、devices(设备链表) |
pci_dev |
表示一个 PCI 功能(Function),包含配置空间、资源、电源状态 | 设备枚举时创建,设备移除时销毁 | devfn(设备/功能号)、vendor/device、resource[](BAR 资源) |
pci_driver |
表示一个 PCI 设备驱动,提供 probe/remove 回调与 ID 表 | 驱动模块加载时注册,卸载时注销 | id_table(匹配表)、probe/remove 函数指针 |
调试提示: 当驱动 probe 失败时,首先检查
pci_dev 的 vendor/device 是否在驱动的 id_table 中;其次检查 resource[] 是否已被 BIOS 或内核正确分配(flags & IORESOURCE_MEM 或 IORESOURCE_IO)。
2.2 数据结构关系图
下面的 SVG 图展示了 pci_bus、pci_dev、pci_driver 之间的关联关系:
2.3 pci_bus:总线拓扑的骨架
pci_bus 结构体是 PCI 总线拓扑的抽象。每个 PCI 段(Segment)下的每条总线都有一个对应的 pci_bus 实例。关键字段包括:
- number:总线号(0~255),用于配置空间访问的 BDF 地址中的 B 字段。
- primary/secondary/subordinate:仅对桥设备有效,定义桥下游的总线范围。
- self:指向代表桥设备本身的
pci_dev实例(根总线为 NULL)。 - devices:挂载在该总线上的所有
pci_dev链表。 - children:子总线链表,形成树形结构。
调试案例: 当设备无法被枚举时,使用
lspci -t 查看总线拓扑,确认设备所在的总线号是否在桥的 subordinate 范围内。如果不在,可能是 BIOS 或 ACPI 分配错误,需通过 pci=assign-buses 内核参数强制重分配。
2.4 pci_dev:设备实例的完整描述
pci_dev 是 PCI 设备在内核中的核心表示。每个 PCI 功能(Function)对应一个 pci_dev。调试时最常关注的字段:
| 字段 | 类型 | 调试用途 |
|---|---|---|
vendor |
u16 | 厂商 ID,与 id_table 匹配 |
device |
u16 | 设备 ID,与 id_table 匹配 |
devfn |
u8 | 编码设备号(高 5 位)和功能号(低 3 位) |
resource[DEVICE_COUNT_RESOURCE] |
struct resource | BAR 空间、ROM、扩展 ROM 的物理地址和长度 |
irq |
unsigned int | 中断号(INTx 或 MSI/MSI-X 分配后) |
current_state |
pci_power_t | 当前电源状态(D0/D3hot/D3cold) |
2.5 pci_driver:驱动与设备的桥梁
pci_driver 是驱动开发者需要实现的核心结构体。注册流程如下:
- 定义 id_table:声明该驱动支持的 vendor/device 列表。
- 实现 probe/remove 回调:probe 中完成硬件初始化、资源申请、设备注册;remove 中做逆操作。
- 调用 pci_register_driver():将驱动注册到 PCI 核心,触发匹配与 probe。
典型的驱动注册代码模板:
#include <linux/pci.h>
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x1234) }, // Vendor=0x10EE, Device=0x1234
{ 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. 初始化硬件、注册字符设备等
// ...
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);
常见崩溃点: probe 中忘记调用
pci_enable_device() 就访问 BAR 空间,会导致 MMIO 访问异常(Oops 或 Machine Check)。另一个常见错误是 pci_request_regions() 失败后未正确回滚,导致后续设备无法使用。
2.6 注册流程详解
从系统启动到驱动 probe 的完整流程如下:
2.7 匹配机制:id_table 与动态 ID
PCI 核心通过 pci_match_id() 函数遍历驱动的 id_table,与 pci_dev 的 vendor/device/subvendor/subdevice/class 进行匹配。匹配规则:
- 精确匹配:vendor + device + subvendor + subdevice 全部匹配。
- 通配符匹配:使用
PCI_ANY_ID(值为 0xFFFF)匹配任意值。 - 类匹配:通过 class 字段匹配设备类别(如存储控制器、网络控制器)。
调试技巧: 如果驱动 probe 未被调用,检查
/sys/bus/pci/drivers/<driver_name>/ 目录下是否有 new_id 文件。可以通过 echo "vendor device" > new_id 动态添加设备 ID,用于临时验证匹配逻辑。
2.8 调试检查清单
当 PCIe 驱动出现 probe 失败或设备不可用时,按以下清单排查:
| 检查项 | 命令/方法 | 预期结果 |
|---|---|---|
| 设备是否被内核识别 | lspci -vvv -s <BDF> |
显示设备信息,无 "Unknown" 或 "Unassigned Class" |
| BAR 是否已分配 | lspci -vvv -s <BDF> 查看 Region |
Region 显示物理地址,非 [disabled] 或 [size=0] |
| 驱动是否已绑定 | ls -l /sys/bus/pci/devices/<BDF>/driver |
指向驱动目录,非 "No such file or directory" |
| 中断是否分配 | cat /proc/interrupts | grep <driver_name> |
显示中断号和计数 |
| dmesg 错误日志 | dmesg | grep -i pci |
无 "resource conflict"、"probe failed" 等错误 |
实战案例: 某 FPGA 加速卡驱动 probe 返回 -ENODEV。排查发现
pci_enable_device() 成功,但 pci_request_regions() 失败。使用 cat /proc/iomem | grep pci 发现 BAR0 地址与系统保留内存区域重叠。最终通过 pci=realloc 内核参数让内核重新分配 BAR 空间解决。