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

在Linux内核中,PCI子系统为设备驱动开发者提供了一套标准化的框架。理解pci_driver结构体、pci_device_id匹配表以及probe/remove回调机制,是编写任何PCIe驱动的基础。本章将深入剖析这三个核心要素,并展示它们如何协同工作,完成从设备识别到驱动初始化的完整流程。

2.1 核心数据结构:pci_driver

每个PCI驱动都需要定义一个pci_driver结构体实例,它是驱动与内核PCI核心之间的桥梁。该结构体包含了驱动的名称、支持的设备列表、以及关键的初始化和退出函数指针。

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);
    // ... 其他高级回调(如shutdown、sriov_configure等)
};
关键点:对于DMA引擎驱动,probe回调是初始化硬件、分配DMA缓冲区和注册DMA通道的地方;remove回调则负责反向清理,确保资源被正确释放。

2.2 设备匹配表:pci_device_id

pci_device_id结构体用于声明该驱动支持哪些PCI设备。内核PCI核心在枚举总线时,会遍历所有驱动的id_table,寻找与设备Vendor ID、Device ID等匹配的条目。

struct pci_device_id {
    __u32 vendor, device;       // 厂商ID和设备ID
    __u32 subvendor, subdevice; // 子系统厂商和设备ID(可设为PCI_ANY_ID)
    __u32 class, class_mask;    // 设备类别和掩码
    unsigned long driver_data;  // 私有数据,可在probe中通过id->driver_data获取
};

匹配表通常以{0, }结尾。以下是一个典型的DMA控制器驱动匹配表示例:

static const struct pci_device_id my_dma_pci_ids[] = {
    { PCI_DEVICE(0x10EE, 0x9034) }, // 厂商0x10EE,设备0x9034
    { PCI_DEVICE(0x10EE, 0x9035) }, // 另一款DMA控制器
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_dma_pci_ids);
最佳实践:使用PCI_DEVICE(vendor, device)宏可以简化赋值。如果驱动支持同一厂商的所有设备,可以使用PCI_VDEVICE(vendor_name, device)宏。务必调用MODULE_DEVICE_TABLE宏,以便内核在模块加载时自动生成别名,实现热插拔支持。

2.3 probe/remove回调机制

当内核发现一个PCI设备,并且其ID与驱动的id_table匹配时,会自动调用驱动的probe函数。反之,当设备被移除或驱动卸载时,会调用remove函数。这一机制是驱动生命周期的核心。

2.3.1 probe回调:驱动初始化入口

probe函数是驱动开始“拥有”一个设备的起点。它接收两个参数:指向pci_dev结构体的指针(代表物理设备)和指向匹配的pci_device_id的指针。

static int my_dma_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    int ret;

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

    // 2. 请求MMIO/BAR资源
    ret = pci_request_regions(pdev, "my_dma_driver");
    if (ret)
        goto err_disable;

    // 3. 设置DMA掩码(64位或32位)
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (ret) {
        ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
        if (ret)
            goto err_regions;
    }

    // 4. 映射BAR空间到CPU虚拟地址
    void __iomem *regs = pci_iomap(pdev, 0, 0);
    if (!regs) {
        ret = -ENOMEM;
        goto err_regions;
    }

    // 5. 初始化硬件(复位、配置DMA引擎等)
    // ...

    // 6. 注册DMA引擎设备(dma_async_device_register)
    // ...

    // 7. 保存私有数据
    pci_set_drvdata(pdev, my_priv_data);

    return 0;

err_regions:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

2.3.2 remove回调:驱动退出与资源清理

remove函数必须撤销probe中执行的所有操作,顺序通常与probe相反。

static void my_dma_remove(struct pci_dev *pdev)
{
    struct my_dma_priv *priv = pci_get_drvdata(pdev);

    // 1. 注销DMA引擎设备
    // dma_async_device_unregister(...);

    // 2. 停止硬件DMA传输
    // ...

    // 3. 释放中断(如果使用了MSI-X)
    // pci_free_irq_vectors(pdev);

    // 4. 取消映射BAR空间
    pci_iounmap(pdev, priv->regs);

    // 5. 释放PCI资源
    pci_release_regions(pdev);

    // 6. 禁用PCI设备
    pci_disable_device(pdev);

    // 7. 释放私有数据结构
    kfree(priv);
}
常见错误:在remove中遗漏了pci_free_irq_vectors或dma_async_device_unregister,会导致内核在模块卸载时崩溃或资源泄漏。务必确保probe和remove是完全对称的。

2.4 驱动注册与注销

在模块的init和exit函数中,需要调用pci_register_driver和pci_unregister_driver来将驱动注册到PCI核心。

static int __init my_dma_init(void)
{
    return pci_register_driver(&my_dma_driver);
}

static void __exit my_dma_exit(void)
{
    pci_unregister_driver(&my_dma_driver);
}

module_init(my_dma_init);
module_exit(my_dma_exit);

2.5 框架工作流程总览

下图展示了从设备插入到驱动初始化的完整流程,以及pci_driver、pci_device_id和probe回调在其中扮演的角色。

PCI子系统驱动匹配与初始化流程 PCI核心(枚举总线) pci_device_id 匹配表 驱动 probe() 回调 pci_enable_device() pci_request_regions() dma_set_mask() pci_iomap() 注册DMA引擎 流程说明 PCI核心枚举设备 → 遍历驱动id_table → 匹配成功 → 调用probe() → 驱动完成硬件初始化和DMA引擎注册

2.6 关键函数与宏总结

函数/宏 作用 调用时机
pci_register_driver() 向内核注册PCI驱动 模块初始化
pci_unregister_driver() 从内核注销PCI驱动 模块退出
pci_enable_device() 启用PCI设备(唤醒、分配I/O和内存资源) probe
pci_request_regions() 请求设备的BAR空间,防止其他驱动占用 probe
pci_iomap() 将BAR物理地址映射到内核虚拟地址空间 probe
dma_set_mask_and_coherent() 设置设备DMA地址宽度(32位/64位) probe
pci_set_drvdata() 将私有数据指针绑定到pci_dev probe
pci_get_drvdata() 从pci_dev获取私有数据指针 remove / 任意回调
MODULE_DEVICE_TABLE() 导出设备ID表,用于热插拔和模块自动加载 模块编译时

2.7 完整驱动骨架示例

将上述所有元素组合起来,就构成了一个完整的PCIe DMA驱动骨架:

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/dmaengine.h>

static const struct pci_device_id my_dma_ids[] = {
    { PCI_DEVICE(0x10EE, 0x9034) },
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_dma_ids);

static int my_dma_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    // ... 实现见2.3.1节
    return 0;
}

static void my_dma_remove(struct pci_dev *pdev)
{
    // ... 实现见2.3.2节
}

static struct pci_driver my_dma_driver = {
    .name     = "my_dma_controller",
    .id_table = my_dma_ids,
    .probe    = my_dma_probe,
    .remove   = my_dma_remove,
};

module_pci_driver(my_dma_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux PCIe DMA Expert");
MODULE_DESCRIPTION("Sample PCIe DMA Engine Driver");
简化注册:使用module_pci_driver()宏可以自动生成module_init和module_exit,使代码更加简洁。这是现代内核驱动推荐的做法。