4. PCIe驱动框架:pci_driver结构体、probe/remove回调、pci_device_id匹配表

PCIe设备驱动的核心骨架由Linux内核提供的pci_driver结构体定义。理解这个结构体及其回调函数,是编写任何PCIe端点驱动的基础。本章将深入剖析驱动注册、设备匹配、初始化和卸载的完整生命周期。

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

在Linux内核中,每个PCIe驱动都必须实例化一个struct pci_driver,并将其注册到PCI核心子系统。该结构体是驱动与内核PCI总线层之间的契约。

成员 类型 说明
name const char * 驱动名称,用于sysfs和调试
id_table const struct pci_device_id * 设备匹配表,驱动支持的设备列表
probe int (*probe)(struct pci_dev *, const struct pci_device_id *) 设备发现时的初始化回调
remove void (*remove)(struct pci_dev *) 设备移除或驱动卸载时的清理回调
suspend / resume 电源管理回调 系统休眠/唤醒时的处理
shutdown void (*shutdown)(struct pci_dev *) 系统关机时的处理
driver struct device_driver 内嵌的通用设备驱动结构体
关键点: probe和remove是驱动开发中最常实现的两个回调。probe负责硬件初始化、资源分配、DMA和中断设置;remove负责反向释放所有资源。

4.2 pci_device_id匹配表:驱动与设备的“握手协议”

当PCIe设备插入系统或驱动加载时,PCI核心会遍历所有已注册驱动的id_table,与设备的Vendor ID、Device ID、Subsystem ID等进行匹配。匹配成功则调用驱动的probe函数。

/* 典型的pci_device_id匹配表 */
static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x10EE, 0x7011) },                 /* 仅匹配VID/PID */
    { PCI_DEVICE_SUB(0x10EE, 0x7012, 0x10EE, 0x0001) }, /* 匹配VID/PID/SubVID/SubPID */
    { PCI_DEVICE_CLASS(0x020000, 0xFFFFFF) },        /* 匹配网络设备类 */
    { 0, }                                           /* 结束标志 */
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
宏 匹配字段 适用场景
PCI_DEVICE(vid, pid) Vendor ID + Device ID 最常用,精确匹配单设备
PCI_DEVICE_SUB(vid, pid, svid, spid) + Subsystem Vendor/Device ID 区分同一芯片的不同板卡版本
PCI_DEVICE_CLASS(class, mask) Class Code 匹配一类设备(如所有存储控制器)
PCI_VDEVICE(vendor, pid) 厂商缩写 + Device ID 使用内核预定义的厂商常量
工程建议: 对于自研FPGA或ASIC PCIe设备,务必在驱动中精确匹配VID/PID,避免误绑定到其他厂商设备。使用lspci -n命令可查看实际设备的VID/PID。

4.3 probe回调:设备初始化的“主战场”

probe函数是驱动开发的核心。当PCI核心发现匹配的设备时,probe被调用,驱动需要在此完成以下关键步骤:

  1. 启用设备:调用pci_enable_device(),激活PCIe配置空间中的I/O和Memory空间。
  2. 设置DMA掩码:调用dma_set_mask()和dma_set_coherent_mask(),告知设备支持的DMA地址宽度(32位或64位)。
  3. 请求BAR资源:调用pci_request_regions(),声明对BAR空间的独占访问。
  4. 映射BAR空间:调用pcim_iomap()或ioremap(),将物理地址映射到内核虚拟地址空间。
  5. 设置DMA和中断:分配DMA缓冲区、注册中断处理函数(MSI-X或INTx)。
  6. 初始化硬件:通过MMIO读写配置设备内部寄存器。
  7. 创建字符/网络设备:注册设备节点,暴露给用户空间。
static int my_pci_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. 设置64位DMA掩码 */
    ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
    if (ret) {
        ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
        if (ret)
            goto err_disable;
    }

    /* 4. 请求并映射BAR0 */
    ret = pci_request_regions(pdev, "my_pci_driver");
    if (ret)
        goto err_disable;

    dev->bar0 = pcim_iomap(pdev, 0, pci_resource_len(pdev, 0));
    if (!dev->bar0) {
        ret = -ENOMEM;
        goto err_release;
    }

    /* 5. 设置MSI-X中断 */
    ret = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX);
    if (ret < 0)
        goto err_release;

    /* 6. 注册中断处理 */
    ret = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, 0),
                           my_interrupt_handler, 0, "my_pci", dev);
    if (ret)
        goto err_free_vectors;

    /* 7. 保存设备指针 */
    pci_set_drvdata(pdev, dev);
    dev->pdev = pdev;

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

err_free_vectors:
    pci_free_irq_vectors(pdev);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
常见错误: 忘记调用pci_enable_device()就进行MMIO访问,会导致总线错误。另外,DMA掩码设置失败后必须回退到32位,否则64位设备在32位系统上无法工作。

4.4 remove回调:优雅的“反向操作”

remove函数是probe的逆过程。当设备热拔出、驱动卸载或系统关机时,内核调用remove。必须确保所有资源被正确释放,避免内存泄漏或内核崩溃。

static void my_pci_remove(struct pci_dev *pdev)
{
    struct my_dev *dev = pci_get_drvdata(pdev);

    /* 1. 停止所有硬件操作(停止DMA、屏蔽中断) */
    writel(0, dev->bar0 + REG_DMA_CTRL);
    writel(0, dev->bar0 + REG_INT_MASK);

    /* 2. 注销中断(devm_*管理的资源自动释放,但显式释放更安全) */
    pci_free_irq_vectors(pdev);

    /* 3. 释放BAR映射和区域(devm_*自动处理,但pci_release_regions需要显式) */
    pci_release_regions(pdev);

    /* 4. 禁用PCIe设备 */
    pci_disable_device(pdev);

    /* 5. 私有数据由devm_kzalloc自动释放 */
    dev_info(&pdev->dev, "device removed\n");
}
最佳实践: 优先使用devm_系列API(如devm_kzalloc、devm_request_irq、pcim_iomap),这些资源会在设备注销时自动释放,减少remove回调中的遗漏风险。

4.5 驱动注册与注销:模块入口/出口

驱动模块的入口和出口函数负责向PCI核心注册/注销pci_driver实例。

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

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

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");
MODULE_AUTHOR("PCIe Driver Expert");

4.6 驱动生命周期流程图

下图展示了从模块加载到设备移除的完整生命周期,以及各阶段调用的关键函数。

PCIe驱动生命周期流程图 PCIe驱动生命周期 module_init → pci_register_driver 设备插入 / 内核扫描 → id_table匹配 probe 回调执行 pci_enable_device → dma_set_mask pci_request_regions → pcim_iomap pci_alloc_irq_vectors → devm_request_irq 设备正常运行(DMA + 中断) 设备热拔出 → remove回调 module_exit → pci_unregister_driver remove回调:资源释放 停止DMA → 屏蔽中断 → 释放BAR → 禁用设备 devm_* API自动释放 部分资源,减少遗漏

4.7 关键数据结构关系图

下图展示了pci_driver、pci_dev、pci_device_id以及内核PCI核心之间的协作关系。

PCIe驱动核心数据结构关系图 PCIe驱动核心数据结构关系 struct pci_driver .name = "my_driver" .id_table → pci_device_id[] .probe → my_probe() .remove → my_remove() pci_device_id[] { 0x10EE, 0x7011 } { 0x10EE, 0x7012 } { 0, 0 } /* 结束 */ 引用 struct pci_dev vendor = 0x10EE device = 0x7011 resource[] → BAR空间 irq → 中断号 匹配后绑定 PCI核心子系统 pci_register_driver() pci_unregister_driver() 设备枚举与匹配 管理 用户空间 /dev/mydev sysfs 设备节点 probe中创建 字符设备

4.8 常见问题与避坑指南

问题 原因 解决方案
probe未被调用 id_table中的VID/PID与实际设备不匹配 使用lspci -n确认设备ID,检查MODULE_DEVICE_TABLE宏
probe返回-ENODEV pci_enable_device失败或DMA掩码设置不当 检查设备是否被BIOS禁用,尝试回退到32位DMA掩码
remove后系统崩溃 DMA仍在进行时释放了缓冲区 在remove中先停止DMA引擎,等待所有传输完成,再释放资源
中断未触发 MSI-X配置错误或中断号注册失败 检查pci_alloc_irq_vectors返回值,确认MSI-X使能寄存器
BAR映射失败 BAR空间被其他驱动占用或未使能 确保pci_request_regions在映射前调用,检查BIOS设置
调试技巧: 在probe和remove函数中添加dev_dbg()或dev_info()打印,配合dmesg观察驱动加载/卸载流程。使用cat /sys/bus/pci/devices/<BDF>/driver确认驱动绑定状态。

4.9 本章小结

本章详细剖析了PCIe驱动框架的核心组件:

  • pci_driver结构体是驱动的入口,定义了名称、匹配表、probe和remove回调。
  • pci_device_id匹配表决定了驱动支持哪些设备,精确匹配是避免冲突的关键。
  • probe回调是设备初始化的主战场,必须按顺序完成使能、DMA设置、资源映射、中断注册。
  • remove回调是probe的逆操作,确保所有资源被安全释放。
  • 优先使用devm_系列API简化资源管理,降低内存泄漏风险。