3. Linux PCIe 子系统架构:核心数据结构与驱动生命周期

在 Linux 内核中,PCIe 子系统为驱动开发者提供了一套标准化的框架。理解 pci_driver 结构体、pci_device_id 匹配表以及 probe/remove 回调函数的生命周期,是编写稳定、高效 PCIe 驱动的基础。本章将深入剖析这三个核心要素,并结合工程案例说明其在实际链路故障排查中的应用。

3.1 pci_driver 结构体:驱动的“身份证”

pci_driver 是 PCIe 驱动与内核之间的契约。每个 PCIe 驱动都必须实例化并注册一个 pci_driver 结构体,内核通过它来识别、匹配和管理驱动。

关键字段解析:
  • name:驱动名称,用于 sysfs 和调试。
  • id_table:指向 pci_device_id 表的指针,定义该驱动支持的设备列表。
  • probe:当设备与驱动匹配成功时调用的初始化函数。
  • remove:设备被移除或驱动卸载时调用的清理函数。
  • shutdown:系统关机时的回调。
  • driver.pm:电源管理操作结构体。
// 一个典型的 pci_driver 定义示例
static struct pci_driver my_pcie_driver = {
    .name       = "my_pcie_device",
    .id_table   = my_pcie_ids,       // 匹配表
    .probe      = my_pcie_probe,     // 探测函数
    .remove     = my_pcie_remove,    // 移除函数
    .shutdown   = my_pcie_shutdown,  // 关机处理
    .driver     = {
        .pm     = &my_pcie_pm_ops,   // 电源管理
    },
};
pci_driver 结构体与内核 PCI 核心的关系 pci_driver 结构体与内核 PCI 核心交互 Linux PCI 核心 pci_driver 实例 (my_pcie_driver) pci_register_driver 核心字段 • name: "my_pcie_device" • id_table: my_pcie_ids • probe: my_pcie_probe • remove: my_pcie_remove • driver.pm: &my_pcie_pm_ops

3.2 pci_device_id 匹配表:驱动与设备的“红娘”

pci_device_id 表定义了驱动所支持的 PCIe 设备集合。内核在枚举 PCIe 总线时,会遍历所有已注册驱动的 id_table,寻找匹配的设备。

字段 说明 匹配规则
vendor 厂商 ID (VID) 必须精确匹配,除非使用 PCI_ANY_ID
device 设备 ID (DID) 必须精确匹配,除非使用 PCI_ANY_ID
subvendor 子系统厂商 ID 可选,用于更精确匹配
subdevice 子系统设备 ID 可选,用于更精确匹配
class 设备类别 可选,匹配大类设备
class_mask 类别掩码 与 class 配合使用
driver_data 驱动私有数据 匹配成功后传递给 probe
工程经验:在链路故障排查中,pci_device_id 表是首要检查点。如果设备无法被驱动绑定,首先确认 VID/DID 是否与硬件规格书一致。使用 lspci -n 命令可以获取实际设备的 VID:DID。
// 匹配表示例:支持多个设备
#define PCI_VENDOR_ID_MY_COMPANY   0x1234
#define PCI_DEVICE_ID_ACCEL_V1     0x5678
#define PCI_DEVICE_ID_ACCEL_V2     0x5679

static const struct pci_device_id my_pcie_ids[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_MY_COMPANY, PCI_DEVICE_ID_ACCEL_V1) },
    { PCI_DEVICE(PCI_VENDOR_ID_MY_COMPANY, PCI_DEVICE_ID_ACCEL_V2) },
    { 0, }  // 必须以空条目结束
};
MODULE_DEVICE_TABLE(pci, my_pcie_ids);
pci_device_id 匹配流程 pci_device_id 匹配流程 PCI 设备枚举 遍历已注册驱动 匹配 id_table 匹配失败 → 继续遍历 匹配成功 → 调用 probe 关键点:匹配优先级为 VID+DID > subvendor+subdevice > class,driver_data 随匹配结果传递

3.3 probe/remove 回调函数生命周期

probe 和 remove 是 PCIe 驱动生命周期中最重要的两个回调函数。它们分别在设备绑定和解除绑定时被内核调用,负责资源的初始化和清理。

3.3.1 probe 回调:驱动的“构造函数”

当 pci_device_id 匹配成功后,内核调用驱动的 probe 函数。典型的 probe 流程包括:

  1. 使能设备:调用 pci_enable_device() 激活 PCIe 设备。
  2. 请求 MMIO/BAR 资源:使用 pci_request_regions() 或 pcim_iomap_regions()。
  3. 设置 DMA 掩码:调用 dma_set_mask_and_coherent()。
  4. 初始化硬件:读写配置空间、复位设备等。
  5. 注册中断:使用 request_irq() 或 pci_alloc_irq_vectors()。
  6. 创建内核接口:如注册字符设备、net_device 等。
static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_dev *dev;
    int ret;

    // 1. 分配私有数据结构
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    // 2. 使能 PCIe 设备
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    // 3. 请求 BAR0 资源并 ioremap
    ret = pcim_iomap_regions(pdev, BIT(0), "my_pcie");
    if (ret)
        goto err_disable;

    dev->regs = pcim_iomap_table(pdev)[0];

    // 4. 设置 DMA 掩码(64 位)
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (ret)
        goto err_disable;

    // 5. 设置驱动私有数据
    pci_set_drvdata(pdev, dev);

    // 6. 注册中断(MSI-X 优先)
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI);
    if (ret < 0)
        goto err_disable;

    dev->irq = pci_irq_vector(pdev, 0);
    ret = request_irq(dev->irq, my_pcie_isr, 0, "my_pcie", dev);
    if (ret)
        goto err_free_vectors;

    dev_info(&pdev->dev, "Probe successful\n");
    return 0;

err_free_vectors:
    pci_free_irq_vectors(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

3.3.2 remove 回调:驱动的“析构函数”

remove 函数负责释放 probe 中分配的所有资源,顺序通常与 probe 相反:

  • 释放中断 → 释放 MSI/MSI-X 向量 → 释放 IOMMU/DMA 映射 → 释放 MMIO 映射 → 禁用设备。
static void my_pcie_remove(struct pci_dev *pdev)
{
    struct my_dev *dev = pci_get_drvdata(pdev);

    // 1. 释放中断
    free_irq(dev->irq, dev);

    // 2. 释放 MSI/MSI-X 向量
    pci_free_irq_vectors(pdev);

    // 3. 禁用设备(自动释放 pcim_iomap_regions 的资源)
    pci_disable_device(pdev);

    // 4. 私有数据由 devm_ 自动释放
    dev_info(&pdev->dev, "Remove complete\n");
}
故障排查要点:在链路故障场景中,probe 失败是常见问题。建议在 probe 的每个关键步骤后添加 dev_dbg() 或 dev_err() 日志。使用 devm_ 系列 API 可以简化错误处理,但需注意 pci_disable_device() 必须显式调用。
probe/remove 生命周期状态机 probe/remove 生命周期状态机 未绑定 Probe 进行中 已绑定 (运行) Remove 进行中 Probe 失败 匹配成功 probe 成功 probe 返回错误 设备移除/驱动卸载 remove 完成 自动回退 生命周期规则:probe 失败后内核自动清理资源(devm_ 系列),remove 必须手动释放非 devm_ 资源 probe 中任何一步失败都应跳转到错误标签,确保不会泄漏资源

3.4 工程案例:probe 失败链路排查

场景:某 FPGA 加速卡在服务器上无法被驱动绑定,dmesg 显示 "my_pcie: probe of 0000:03:00.0 failed with error -16"。

排查步骤:

步骤 操作 预期结果
1 检查 lspci -vvv -s 03:00.0 确认设备状态 确认设备处于正常枚举状态
2 查看 /sys/bus/pci/devices/0000:03:00.0/driver 链接 确认当前是否已被其他驱动绑定
3 检查 pci_enable_device 返回值 -16 对应 -EBUSY,设备已被占用
4 使用 lspci -k 查看内核驱动绑定情况 发现设备被 "vfio-pci" 驱动绑定
5 解绑冲突驱动:echo 0000:03:00.0 > /sys/bus/pci/drivers/vfio-pci/unbind 释放设备
6 重新绑定目标驱动:echo 0000:03:00.0 > /sys/bus/pci/drivers/my_pcie/bind probe 成功
最佳实践:在 probe 函数中,始终使用 dev_err() 输出错误码和上下文信息。例如:dev_err(&pdev->dev, "Failed to enable device: %d\n", ret);。这能大幅缩短故障定位时间。