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 之间的关联关系:

PCIe 子系统核心数据结构关系 pci_bus number: 总线号 self: 指向桥 pci_dev devices: 设备链表头 pci_dev devfn: 设备/功能号 vendor/device: ID resource[]: BAR 资源 pci_driver id_table: 匹配表 probe/remove 回调 driver.name: 驱动名 pci_bus 通过 devices 链表管理多个 pci_dev pci_driver 通过 id_table 匹配 pci_dev,匹配成功后调用 probe 总线扫描 设备枚举 驱动匹配 生命周期:pci_bus → pci_dev → pci_driver.probe()

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 是驱动开发者需要实现的核心结构体。注册流程如下:

  1. 定义 id_table:声明该驱动支持的 vendor/device 列表。
  2. 实现 probe/remove 回调:probe 中完成硬件初始化、资源申请、设备注册;remove 中做逆操作。
  3. 调用 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 的完整流程如下:

PCIe 设备注册流程 1. 总线扫描 2. 创建 pci_dev 3. 分配资源(BAR) 4. 驱动注册 5. 匹配与 probe 6. 设备就绪 步骤 3 由 PCI 核心在枚举时完成;步骤 4 由驱动模块加载时触发

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 空间解决。