2. Linux PCI子系统框架:pci_driver结构体、probe/remove回调、设备ID表匹配机制

在Linux内核中,PCI子系统为设备驱动开发者提供了高度抽象化的框架。编写一个PCIe设备驱动的核心工作,就是填充一个 pci_driver 结构体,并注册到内核。本章将深入剖析这个框架的三个关键要素:驱动核心结构体、生命周期回调函数以及设备匹配机制。

核心要点: PCI驱动模型基于“设备-驱动”分离的架构。内核通过设备ID表自动匹配硬件设备,并调用驱动的 probe 回调完成初始化;当设备移除或驱动卸载时,调用 remove 回调进行清理。

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

pci_driver 是PCI驱动与内核之间的契约。驱动必须定义并初始化该结构体的关键字段,然后通过 pci_register_driver() 将其注册到PCI核心。

以下是 pci_driver 结构体的核心成员定义(位于 include/linux/pci.h):

struct pci_driver {
    const char *name;                    // 驱动名称,用于sysfs标识
    const struct pci_device_id *id_table; // 设备ID表,用于匹配硬件
    int  (*probe)(struct pci_dev *dev, const struct pci_device_id *id);
    void (*remove)(struct pci_dev *dev);
    int  (*suspend)(struct pci_dev *dev, pm_message_t state);
    int  (*resume)(struct pci_dev *dev);
    void (*shutdown)(struct pci_dev *dev);
    struct device_driver driver;         // 内嵌通用驱动结构
    // ... 其他高级字段(如SR-IOV、错误处理等)
};
pci_driver 核心字段说明
字段 类型 作用
name const char * 驱动名称,在 /sys/bus/pci/drivers/ 下可见
id_table const struct pci_device_id * 指向设备ID数组的指针,决定驱动支持哪些硬件
probe 回调函数指针 设备匹配成功时调用,负责初始化设备、分配资源、注册DMA缓冲区等
remove 回调函数指针 设备移除或驱动卸载时调用,负责释放资源、停止DMA传输
suspend / resume 回调函数指针 电源管理回调,用于系统休眠/唤醒场景
最佳实践: 在 probe 中完成所有资源分配(包括DMA缓冲区和环形缓冲区),在 remove 中按相反顺序释放。这能有效避免资源泄漏。

2.2 probe/remove回调:驱动的生命周期

probe 和 remove 是驱动中最重要的两个回调函数,它们定义了设备从“被发现”到“被移除”的完整生命周期。

2.2.1 probe回调:设备初始化入口

当PCI核心发现一个设备的Vendor ID、Device ID与驱动的 id_table 匹配时,会调用驱动的 probe 函数。典型的 probe 流程包括:

  1. 启用PCI设备: 调用 pci_enable_device() 激活设备。
  2. 设置DMA掩码: 调用 dma_set_mask() 和 dma_set_coherent_mask() 告知设备支持的地址宽度(如64位)。
  3. 请求MMIO/BAR资源: 调用 pci_request_regions() 获取BAR空间。
  4. 映射BAR到虚拟地址: 调用 pcim_iomap() 或 ioremap() 将物理地址映射到内核虚拟地址空间。
  5. 设置DMA传输引擎: 初始化环形缓冲区、分配DMA缓冲池、注册中断处理函数。
  6. 注册设备: 调用 pci_set_drvdata() 保存私有数据结构指针。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_device *mydev;
    int ret;

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

    // 2. 启用PCI设备
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    // 3. 设置64位DMA掩码
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (ret) {
        dev_err(&pdev->dev, "No suitable DMA mask available\n");
        goto err_disable;
    }

    // 4. 请求并映射BAR0
    ret = pcim_iomap_regions(pdev, BIT(0), KBUILD_MODNAME);
    if (ret)
        goto err_disable;

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

    // 5. 初始化环形缓冲区与DMA(后续章节详述)
    // ret = my_init_ring_buffer(mydev);
    // if (ret) goto err_disable;

    // 6. 注册中断(MSI-X优先)
    // ret = request_irq(...);

    dev_info(&pdev->dev, "Device probed successfully\n");
    return 0;

err_disable:
    pci_disable_device(pdev);
    return ret;
}

2.2.2 remove回调:设备清理出口

remove 回调负责撤销 probe 中执行的所有操作。内核保证 remove 在设备热拔插、驱动卸载或系统关闭时被调用。

static void my_pci_remove(struct pci_dev *pdev)
{
    struct my_device *mydev = pci_get_drvdata(pdev);

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

    // 2. 停止DMA传输并释放环形缓冲区
    // my_cleanup_ring_buffer(mydev);

    // 3. 释放BAR映射(pcim_iomap_regions 自动管理)
    // 4. 禁用设备
    pci_disable_device(pdev);

    // 5. 私有数据由 devm_kzalloc 自动释放
    dev_info(&pdev->dev, "Device removed\n");
}
注意: 如果使用了 devm_* 系列API(如 devm_kzalloc、pcim_iomap_regions),内核会在设备分离时自动释放资源。但DMA描述符、环形缓冲区指针等仍需在 remove 中手动清理。

2.3 设备ID表匹配机制

设备ID表是驱动与硬件之间的“桥梁”。内核通过遍历驱动的 id_table,与每个PCI设备的Vendor ID、Device ID、Subsystem ID等字段进行匹配。

2.3.1 pci_device_id结构体

struct pci_device_id {
    __u32 vendor, device;      // 主 Vendor ID 和 Device ID
    __u32 subvendor, subdevice;// Subsystem Vendor ID 和 Device ID
    __u32 class, class_mask;   // 设备类别与掩码
    unsigned long driver_data; // 驱动私有数据(如硬件版本标识)
};
匹配字段说明
字段 匹配规则 示例值
vendor 必须完全匹配,PCI_ANY_ID 表示通配 0x10EE (Xilinx)
device 必须完全匹配,PCI_ANY_ID 表示通配 0x7021 (DMA控制器)
subvendor / subdevice 用于区分同一芯片的不同板卡版本 PCI_ANY_ID
class / class_mask 按设备类别匹配(如存储控制器、网络控制器) 0x020000 (以太网)
driver_data 匹配成功后传递给 probe 的私有数据 0x01 (硬件版本A)

2.3.2 匹配优先级与流程

PCI核心的匹配顺序遵循“精确匹配优先”原则:

  1. 精确匹配: vendor + device + subvendor + subdevice 全部匹配。
  2. 通配匹配: vendor + device 匹配,subvendor/subdevice 为 PCI_ANY_ID。
  3. 类别匹配: 通过 class 和 class_mask 进行范围匹配。
匹配示例: 一个Xilinx DMA设备(Vendor=0x10EE, Device=0x7021)会依次检查驱动表中的所有条目,直到找到 vendor=0x10EE 且 device=0x7021 的条目。如果驱动同时定义了 subvendor=0x10EE, subdevice=0x0001,则只有特定版本的板卡才会匹配。

2.3.3 设备ID表定义示例

static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x10EE, 0x7021),        // 匹配所有Xilinx 0x7021设备
      .driver_data = MY_HW_V1 },
    { PCI_DEVICE_SUB(0x10EE, 0x7022,     // 匹配特定子系统的设备
                     0x10EE, 0x0002),
      .driver_data = MY_HW_V2 },
    { 0, }  // 终止条目
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

2.4 整体框架流程图

下图展示了从驱动注册到设备匹配、probe/remove调用的完整流程:

PCI驱动框架:注册、匹配与生命周期 驱动模块加载 pci_register_driver() PCI核心:添加驱动到总线 系统启动 / 设备热插拔 PCI核心:枚举设备 创建pci_dev对象 设备ID表匹配 匹配成功? 遍历 id_table 条目 匹配失败 匹配成功 调用 probe() 回调 设备就绪,DMA传输 remove() 清理资源

2.5 关键数据结构关系图

以下SVG展示了 pci_driver、pci_device_id、pci_dev 以及内核PCI核心之间的静态关系:

PCI驱动核心数据结构关系 struct pci_driver name: "my_pci_driver" id_table → pci_device_id[] probe() → my_pci_probe remove() → my_pci_remove pci_device_id[] { vendor: 0x10EE, device: 0x7021 } { vendor: 0x10EE, device: 0x7022 } { 0, 0 } /* 终止 */ struct pci_dev vendor: 0x10EE device: 0x7021 driver → pci_driver PCI核心子系统 遍历 id_table → 匹配 pci_dev → 调用 probe()

2.6 驱动注册与注销示例

完整的PCI驱动入口点通常使用 module_init / module_exit 宏:

static int __init my_pci_init(void)
{
    int ret;
    ret = pci_register_driver(&my_pci_driver);
    if (ret)
        pr_err("Failed to register PCI driver: %d\n", ret);
    return ret;
}

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

module_init(my_pci_init);
module_exit(my_pci_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("PCIe DMA Driver - Ring Buffer Example");
调试技巧: 使用 modinfo my_driver.ko 查看驱动的设备ID表;使用 lspci -v 查看当前系统中PCI设备的Vendor/Device ID,验证匹配是否生效。

2.7 本章小结

  • pci_driver 是PCI驱动的核心结构体,必须填充 name、id_table、probe 和 remove 字段。
  • probe 回调负责设备初始化,包括启用设备、设置DMA掩码、映射BAR、分配环形缓冲区;remove 回调负责逆序清理。
  • 设备ID表 通过 pci_device_id 数组定义,支持精确匹配、通配匹配和类别匹配,匹配成功后内核自动调用 probe。
  • 使用 MODULE_DEVICE_TABLE(pci, ...) 导出ID表,使内核在模块加载前即可进行设备匹配。