4. Linux PCI 子系统:pci_driver 结构、pci_device_id 匹配、probe/remove 回调

在 Linux 内核中,PCI 子系统为驱动开发者提供了标准化的设备-驱动模型。任何 PCIe 端点或桥设备要正常工作,都必须通过 pci_driver 结构体向内核注册,并利用 pci_device_id 表完成设备与驱动的匹配。匹配成功后,内核自动调用 probe 回调;设备移除时调用 remove 回调。理解这三个核心要素,是编写稳定、高性能 PCIe 驱动的基础。

4.1 pci_driver 结构体

pci_driver 是 PCI 设备驱动的核心描述符,定义在 <linux/pci.h> 中。它封装了驱动名称、支持的设备 ID 列表、以及生命周期回调函数。

成员 类型 说明
name const char * 驱动名称,用于 sysfs 和调试
id_table const struct pci_device_id * 驱动支持的设备 ID 列表,匹配依据
probe int (*probe)(struct pci_dev *, const struct pci_device_id *) 设备匹配成功后的初始化入口
remove void (*remove)(struct pci_dev *) 设备移除或驱动卸载时的清理出口
suspend int (*suspend)(struct pci_dev *, pm_message_t) 电源管理挂起回调(可选)
resume int (*resume)(struct pci_dev *) 电源管理恢复回调(可选)
shutdown void (*shutdown)(struct pci_dev *) 系统关机时的回调(可选)
driver struct device_driver 内嵌通用设备驱动结构体
关键点: probe 和 remove 是必须实现的回调。如果驱动不需要电源管理,suspend/resume 可以设为 NULL。

4.2 pci_device_id 匹配机制

pci_device_id 结构体定义了驱动所关心的设备特征。内核 PCI 核心在枚举总线时,会遍历每个设备的配置空间,提取 Vendor ID、Device ID 等信息,并与所有已注册驱动的 id_table 进行匹配。

struct pci_device_id {
    __u32 vendor, device;      /* 厂商ID、设备ID */
    __u32 subvendor, subdevice;/* 子系统厂商ID、子系统设备ID */
    __u32 class, class_mask;   /* 类别码及掩码 */
    unsigned long driver_data; /* 驱动私有数据 */
};

匹配优先级规则如下:

  • 精确匹配: vendor + device + subvendor + subdevice 全部匹配。
  • 通配匹配: 使用 PCI_ANY_ID(值为 0xFFFF)表示任意值。
  • 类别匹配: 通过 class 和 class_mask 匹配设备类别。
  • 匹配顺序: 内核按 id_table 数组顺序依次匹配,返回第一个成功的条目。
最佳实践: 对于自定义 PCIe 设备,建议使用精确匹配(vendor + device + subvendor + subdevice),避免与其他设备冲突。对于通用类驱动(如 NVMe、XHCI),使用类别匹配更合适。

4.3 probe 回调:设备初始化

当 pci_device_id 匹配成功后,内核调用驱动的 probe 函数。这是驱动获取设备资源、建立 DMA 映射、注册中断、创建内核数据结构的入口。

典型的 probe 流程:

  1. 启用设备: 调用 pci_enable_device() 激活 PCI 设备。
  2. 请求 MMIO/BAR 资源: 使用 pci_request_regions() 或 pcim_iomap_regions() 获取 BAR 地址空间。
  3. 设置 DMA 掩码: 调用 dma_set_mask_and_coherent() 告知设备支持的 DMA 地址宽度。
  4. 分配中断: 使用 pci_alloc_irq_vectors() 分配 MSI/MSI-X 或传统 INTx 中断。
  5. 初始化硬件: 通过 MMIO 读写配置设备内部寄存器。
  6. 创建数据结构: 分配私有数据结构,初始化锁、工作队列、DMA 缓冲池等。
  7. 注册设备: 调用 pci_set_drvdata() 保存私有数据指针,注册 misc 或字符设备。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_dev *dev;
    int ret;

    ret = pcim_enable_device(pdev);  /* 自动资源管理 */
    if (ret)
        return ret;

    ret = pcim_iomap_regions(pdev, BIT(0), "my_driver");
    if (ret)
        return ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->bar0 = pcim_iomap_table(pdev)[0];
    dev->pdev = pdev;
    pci_set_drvdata(pdev, dev);

    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (ret)
        return ret;

    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        return ret;

    dev->irq = pci_irq_vector(pdev, 0);
    ret = devm_request_irq(&pdev->dev, dev->irq, my_isr, 0, "my_driver", dev);
    if (ret)
        return ret;

    /* 注册字符设备或 misc 设备 */
    return 0;
}

4.4 remove 回调:设备清理

remove 回调在设备热拔出、驱动卸载或系统关机时被调用。它的职责是 probe 的逆操作,确保所有资源被正确释放,避免内存泄漏或中断悬空。

典型的 remove 流程:

  • 停止所有正在进行的 DMA 传输和硬件操作。
  • 注销中断处理函数(使用 devm_ 系列 API 可自动完成)。
  • 释放 DMA 缓冲区和环形队列。
  • 注销字符设备或 misc 设备节点。
  • 内核自动释放 devm_ 管理的资源(MMIO 映射、内存、中断)。
static void my_pci_remove(struct pci_dev *pdev)
{
    struct my_dev *dev = pci_get_drvdata(pdev);

    /* 停止硬件操作 */
    writel(0, dev->bar0 + REG_CTRL);

    /* 注销设备节点(如果手动注册) */
    /* misc_deregister(&dev->miscdev); */

    /* devm_ 系列 API 会自动释放资源,无需手动调用 */
}
注意: 如果 probe 中使用了非 devm_ 的 API(如 pci_enable_device 而非 pcim_enable_device),则必须在 remove 中手动调用 pci_disable_device()、pci_release_regions() 等逆操作。推荐优先使用 pcim_ 和 devm_ 系列 API 简化资源管理。

4.5 驱动注册与注销

驱动模块的入口和出口函数负责向内核注册 pci_driver:

static struct pci_driver my_pci_driver = {
    .name     = "my_pcie_driver",
    .id_table = my_pci_ids,
    .probe    = my_pci_probe,
    .remove   = my_pci_remove,
};

static int __init my_driver_init(void)
{
    return pci_register_driver(&my_pci_driver);
}

static void __exit my_driver_exit(void)
{
    pci_unregister_driver(&my_pci_driver);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

4.6 设备-驱动匹配与生命周期 SVG 图

下图展示了从 PCI 枚举到驱动 probe/remove 的完整生命周期:

PCI 设备-驱动匹配与生命周期 PCI 设备-驱动匹配与生命周期 PCI 核心枚举总线 读取 Vendor/Device ID 匹配 pci_device_id 匹配成功 调用 probe 回调 启用设备 / 映射 BAR 设置 DMA 掩码 / 中断 初始化硬件 / 注册设备 设备就绪,提供服务 设备移除 / 驱动卸载 调用 remove 回调 probe 失败时返回负错误码

4.7 关键数据结构关系图

下图展示了 pci_driver、pci_device_id、pci_dev 之间的核心关系:

PCI 驱动核心数据结构关系 pci_driver / pci_device_id / pci_dev 关系 struct pci_driver .name = "my_driver" .id_table → pci_device_id[] .probe → my_pci_probe() .remove → my_pci_remove() .suspend / .resume (可选) pci_device_id[] { vendor, device, subvendor, subdevice, class, class_mask, driver_data } PCI_ANY_ID 表示通配 按数组顺序匹配 struct pci_dev vendor / device subsystem_vendor subsystem_device class resource[] (BAR) irq dev (device_driver) 引用 匹配 probe 回调接收 pci_dev 指针和匹配到的 pci_device_id 指针,driver_data 可传递平台特定参数

4.8 常见问题与避坑指南

问题 原因 解决方案
probe 未被调用 id_table 未匹配或驱动未注册 检查 vendor/device ID 是否正确;确认 pci_register_driver 成功
probe 返回 -ENODEV 设备资源不足或硬件初始化失败 检查 BAR 是否已分配;确认 DMA 掩码设置正确
remove 后设备仍可访问 未正确停止硬件 DMA 或未注销中断 在 remove 中先写寄存器停止硬件,再释放资源
多个设备共用一个驱动 id_table 包含多个条目 利用 driver_data 区分不同设备型号的行为
热拔出导致内核崩溃 remove 回调未正确处理正在进行的 I/O 使用引用计数或 completion 机制确保所有 I/O 完成后再释放
调试技巧: 在 probe 和 remove 中添加 dev_info() 打印,配合 lspci -vvv 和 cat /sys/bus/pci/devices/.../driver 确认驱动绑定状态。