3. PCI设备驱动框架:pci_driver结构体、probe/remove回调、设备ID表、模块化注册

PCI(Peripheral Component Interconnect)设备驱动是Linux网络设备驱动中最典型的实现路径。无论是千兆以太网卡还是万兆光纤适配器,其驱动核心都围绕一个统一的框架展开:pci_driver。本章将拆解这个框架的四个关键要素:驱动结构体、探测/移除回调、设备ID匹配表以及模块化注册流程。

核心要点: PCI驱动框架的本质是“内核-驱动-设备”的三方契约。驱动通过pci_driver结构体向内核声明能力,内核通过ID表匹配设备,probe回调是驱动与设备第一次握手的入口。

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

在Linux内核中,每个PCI驱动都必须实例化一个struct 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);
    // ... 其他高级回调(shutdown、sriov_configure等)
};
pci_driver 核心字段说明
字段 类型 作用
name const char * 驱动名称,在 /sys/bus/pci/drivers/ 下可见
id_table const struct pci_device_id * 指向设备ID数组,内核据此判断驱动能否管理某设备
probe 回调函数指针 设备匹配成功后调用,完成硬件初始化、资源分配、netdev注册
remove 回调函数指针 设备移除或驱动卸载时调用,释放probe分配的所有资源
suspend/resume 回调函数指针 电源管理,支持系统休眠/唤醒(网络驱动通常必须实现)
开发经验: 在编写probe回调之前,先完整定义pci_driver结构体。name字段建议使用“厂商_设备”格式(如“e1000_pro”),便于调试时快速识别。
pci_driver 结构体与内核PCI核心的关系 Linux PCI Core pci_register_driver() pci_driver my_net_driver .name .id_table .probe .remove PCI 设备 Vendor/Device ID 流程: PCI Core 扫描总线 → 读取设备 Vendor/Device ID → 遍历已注册驱动的 id_table → 匹配成功 → 调用 probe 设备热拔出或驱动卸载 → 调用 remove → 释放资源 → 注销驱动

3.2 probe/remove回调:驱动的初始化和善后

probe 是PCI驱动中最重要的函数。当内核发现一个PCI设备的Vendor ID和Device ID与驱动的id_table匹配时,就会调用probe。probe的职责是“让设备准备好被使用”。

remove 是probe的逆操作。当设备被物理移除、驱动被卸载或系统进入低功耗状态时,内核调用remove。remove必须释放probe分配的所有资源,否则会导致内存泄漏或内核崩溃。

probe 典型执行步骤

  1. 启用PCI设备: 调用 pci_enable_device(),激活设备上的I/O和内存空间。
  2. 请求资源: 调用 pci_request_regions(),声明对BAR空间的独占访问。
  3. 设置DMA掩码: 调用 dma_set_mask() 和 dma_set_coherent_mask(),告知设备可用地址范围。
  4. 映射BAR空间: 使用 pci_iomap() 将设备寄存器映射到内核虚拟地址空间。
  5. 初始化私有数据结构: 分配 net_device 和私有数据块,设置硬件操作函数集 netdev_ops。
  6. 注册网络设备: 调用 register_netdev(),使设备对网络层可见。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *netdev;
    struct my_adapter *adapter;
    int ret;

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

    // 2. 请求I/O和内存区域
    ret = pci_request_regions(pdev, DRV_NAME);
    if (ret)
        goto err_disable;

    // 3. 设置DMA掩码(64位优先,回退到32位)
    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_release;
    }

    // 4. 分配net_device + 私有数据
    netdev = alloc_etherdev(sizeof(struct my_adapter));
    if (!netdev) {
        ret = -ENOMEM;
        goto err_release;
    }
    adapter = netdev_priv(netdev);
    adapter->pdev = pdev;
    adapter->netdev = netdev;

    // 5. 映射BAR0寄存器空间
    adapter->hw_addr = pci_iomap(pdev, 0, 0);
    if (!adapter->hw_addr) {
        ret = -EIO;
        goto err_free_netdev;
    }

    // 6. 设置netdev操作函数
    netdev->netdev_ops = &my_netdev_ops;
    netdev->ethtool_ops = &my_ethtool_ops;

    // 7. 注册网络设备
    ret = register_netdev(netdev);
    if (ret)
        goto err_iounmap;

    // 保存pci_drvdata便于remove访问
    pci_set_drvdata(pdev, netdev);

    return 0;

err_iounmap:
    pci_iounmap(pdev, adapter->hw_addr);
err_free_netdev:
    free_netdev(netdev);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
常见错误: probe中任何一步失败都必须回滚之前所有成功的操作。上述代码使用goto标签实现了清晰的错误处理链,这是内核驱动的标准实践。不要在probe中直接返回而不释放已分配资源。

remove 回调实现

static void my_pci_remove(struct pci_dev *pdev)
{
    struct net_device *netdev = pci_get_drvdata(pdev);
    struct my_adapter *adapter = netdev_priv(netdev);

    // 1. 注销网络设备(必须在释放资源之前)
    unregister_netdev(netdev);

    // 2. 取消映射寄存器
    pci_iounmap(pdev, adapter->hw_addr);

    // 3. 释放net_device
    free_netdev(netdev);

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

    // 5. 禁用PCI设备
    pci_disable_device(pdev);
}
probe / remove 生命周期与资源管理 probe 开始 pci_enable_device() pci_request_regions() dma_set_mask() alloc_etherdev() pci_iomap() register_netdev() remove 开始 unregister_netdev() pci_iounmap() free_netdev() pci_release_regions() pci_disable_device() 设备移除/驱动卸载 注意:remove 的执行顺序与 probe 完全相反,确保资源按栈式释放(LIFO)。

3.3 设备ID表:驱动与硬件的匹配规则

设备ID表是驱动声明“我能管理哪些硬件”的清单。内核PCI核心在枚举总线时,会读取每个PCI设备的Vendor ID、Device ID等信息,然后遍历所有已注册驱动的id_table,寻找匹配项。

// 定义于 include/linux/mod_devicetable.h
struct pci_device_id {
    __u32 vendor, device;      // 厂商ID和设备ID
    __u32 subvendor, subdevice; // 子系统厂商ID和设备ID(可设为PCI_ANY_ID)
    __u32 class, class_mask;   // 设备类别和掩码
    unsigned long driver_data; // 驱动私有数据,probe时通过id->driver_data获取
};
pci_device_id 匹配模式示例
匹配模式 vendor device subvendor subdevice 说明
精确匹配 0x8086 0x10D3 0x8086 0x0001 仅匹配特定厂商的特定子型号
通配子系统 0x8086 0x10D3 PCI_ANY_ID PCI_ANY_ID 匹配所有使用该芯片的设备
类别匹配 PCI_ANY_ID PCI_ANY_ID PCI_ANY_ID PCI_ANY_ID 配合class字段匹配网络控制器类别
// 实际驱动中的ID表定义示例
#define PCI_VENDOR_ID_MYVENDOR  0x1234
#define PCI_DEVICE_ID_MYCHIP_A 0x5678
#define PCI_DEVICE_ID_MYCHIP_B 0x5679

static const struct pci_device_id my_pci_tbl[] = {
    { PCI_VDEVICE(MYVENDOR, PCI_DEVICE_ID_MYCHIP_A), .driver_data = CHIP_A },
    { PCI_VDEVICE(MYVENDOR, PCI_DEVICE_ID_MYCHIP_B), .driver_data = CHIP_B },
    { 0, }  // 必须以空条目结束
};
MODULE_DEVICE_TABLE(pci, my_pci_tbl);  // 导出给用户空间热插拔工具
最佳实践: 使用 PCI_VDEVICE 宏可以自动填充vendor字段,减少重复代码。driver_data字段用于区分同一驱动支持的不同硬件版本,probe中通过 id->driver_data 获取。

3.4 模块化注册:pci_register_driver / pci_unregister_driver

驱动模块的入口和出口函数负责向内核注册和注销pci_driver。这是模块化编程的标准模式,通常放在模块的init和exit函数中。

static struct pci_driver my_pci_driver = {
    .name     = DRV_NAME,
    .id_table = my_pci_tbl,
    .probe    = my_pci_probe,
    .remove   = my_pci_remove,
    // 电源管理回调(可选但推荐)
    .driver.pm = &my_pm_ops,
};

static int __init my_driver_init(void)
{
    int ret;

    pr_info("Loading %s PCI driver\n", DRV_NAME);

    // 注册PCI驱动
    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_driver_exit(void)
{
    pr_info("Unloading %s PCI driver\n", DRV_NAME);
    pci_unregister_driver(&my_pci_driver);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My PCI Ethernet Driver");
关键约束: pci_register_driver() 是同步调用。在函数返回之前,内核会对所有已枚举的PCI设备进行ID匹配,并对匹配成功的设备立即调用probe。因此,probe回调中的所有依赖(如其他子系统、固件加载)必须在init函数中提前准备好。
模块化注册与PCI驱动生命周期 insmod / modprobe module_init() pci_register_driver() 遍历已枚举PCI设备 ID表匹配 调用 probe() rmmod module_exit() pci_unregister_driver() 关键时序:pci_register_driver() 返回前,所有匹配设备的 probe 已经执行完毕。不要在 probe 中依赖尚未初始化的全局资源。

3.5 本章小结

PCI设备驱动框架是网络驱动开发的基石。理解pci_driver结构体的每个字段、probe/remove的对称资源管理、设备ID表的匹配机制以及模块化注册的时序,是编写稳定、可移植网络驱动的第一步。在下一章中,我们将深入probe内部,详细讲解如何初始化PCI设备、分配DMA缓冲区以及注册net_device。

PCI驱动框架关键要素速查
要素 核心API/宏 注意事项
驱动结构体 struct pci_driver name字段必须唯一,避免与现有驱动冲突
设备ID表 struct pci_device_id + MODULE_DEVICE_TABLE 必须以空条目 {0,} 结尾;使用PCI_ANY_ID通配
probe回调 pci_enable_device / pci_request_regions / register_netdev 每一步失败都必须回滚;使用goto错误处理链
remove回调 unregister_netdev / pci_iounmap / pci_release_regions 执行顺序与probe相反;确保所有DMA操作已停止
模块注册 pci_register_driver / pci_unregister_driver 注册是同步的;probe可能在注册函数内被调用