4. Linux 内核 PCI 子系统:pci_dev 结构体、pci_driver 注册流程、probe/remove 回调

PCI 子系统是 Linux 内核中连接物理 PCI/PCIe 设备与设备驱动程序的桥梁。理解 pci_dev 结构体、驱动注册流程以及 probe/remove 回调机制,是进行 PCIe 驱动移植与多队列配置的基础。本章将深入剖析这三个核心要素。

4.1 pci_dev 结构体:设备的软件抽象

在 Linux 内核中,每一个物理 PCI 或 PCIe 设备(或功能)都由一个 struct pci_dev 实例表示。该结构体包含了设备的所有硬件属性、资源映射以及状态信息,是驱动与硬件交互的入口。

核心字段解析: 驱动在 probe 回调中通过 pci_dev 指针获取设备资源,包括 BAR 地址、中断号、设备 ID 等。
字段 类型 说明
dev struct device 通用设备模型基类,用于电源管理、sysfs 等。
vendor / device unsigned int 厂商 ID 和设备 ID,用于驱动匹配。
subsystem_vendor / subsystem_device unsigned int 子系统厂商和设备 ID,用于更精确的匹配。
class unsigned int 设备类别代码(如网卡、存储控制器)。
irq unsigned int 传统 INTx 中断号。对于 MSI/MSI-X,需通过 pci_irq_vector() 获取。
resource[] struct resource BAR 空间资源数组,最多 6 个(PCIe 最多 6 个 BAR)。
msi_enabled / msix_enabled int 标志位,指示 MSI 或 MSI-X 是否已启用。
driver struct pci_driver * 指向当前绑定到此设备的驱动实例。
移植要点: 在 MSI-X 多队列场景下,驱动应优先使用 pci_irq_vector() 而非 pci_dev->irq,因为后者仅代表传统中断线。
pci_dev 结构体核心字段关系图 struct pci_dev vendor / device irq / msi_enabled resource[6] driver 关键说明 • vendor/device 用于驱动与设备的静态匹配。 • resource[] 存储 BAR 物理地址和长度,驱动通过 ioremap() 映射。 • irq 字段在 MSI-X 启用后不再代表实际中断向量。

4.2 pci_driver 注册流程

每个 PCIe 设备驱动都需要定义一个 struct pci_driver 实例,并通过 pci_register_driver() 向内核注册。注册后,内核 PCI 核心层会遍历所有未绑定的设备,根据 ID 表进行匹配,匹配成功后调用驱动的 probe 回调。

4.2.1 pci_driver 结构体定义

static struct pci_driver my_pci_driver = {
    .name     = "my_pcie_device",
    .id_table = my_pci_ids,        // 支持的设备 ID 列表
    .probe    = my_pci_probe,      // 设备发现回调
    .remove   = my_pci_remove,     // 设备移除回调
    .shutdown = my_pci_shutdown,   // 系统关机回调(可选)
    .driver   = {
        .pm = &my_pci_pm_ops,      // 电源管理操作(可选)
    },
};

4.2.2 注册与注销 API

函数 说明 调用时机
pci_register_driver() 注册驱动到 PCI 子系统,触发设备匹配与 probe。 模块初始化入口(module_init)
pci_unregister_driver() 注销驱动,触发所有已绑定设备的 remove 回调。 模块卸载入口(module_exit)
常见错误: 忘记在 id_table 中包含正确的 vendor 和 device ID,或者 ID 表以空条目结尾,会导致驱动永远无法匹配到设备。
pci_driver 注册与匹配流程 模块加载 pci_register_driver() 遍历 PCI 设备列表 ID 匹配成功? 跳过,继续遍历 调用 probe()

4.3 probe 与 remove 回调

probe 和 remove 是驱动生命周期中最重要的两个回调函数,分别负责设备的初始化和清理工作。

4.3.1 probe 回调:设备初始化

当 PCI 核心层找到一个匹配的设备时,会调用驱动的 probe 函数。驱动在此函数中完成以下典型步骤:

  1. 启用设备: 调用 pci_enable_device() 开启设备电源和 I/O 访问。
  2. 设置 DMA 掩码: 调用 dma_set_mask() 和 dma_set_coherent_mask() 配置设备可访问的地址范围。
  3. 获取 BAR 资源: 使用 pci_resource_start() / pci_resource_len() 获取 BAR 物理地址和长度,并通过 ioremap() 映射到内核虚拟地址空间。
  4. 分配中断: 调用 pci_alloc_irq_vectors() 请求 MSI-X 或 MSI 中断向量(详见后续章节)。
  5. 初始化硬件: 通过映射后的寄存器地址配置设备内部状态。
  6. 注册设备: 将设备注册到相应的内核子系统(如网络、存储、字符设备)。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_dev *dev;
    int ret;

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

    // 2. 设置 DMA 掩码(64 位寻址示例)
    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;
    }

    // 3. 分配驱动私有数据结构
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        ret = -ENOMEM;
        goto err_disable;
    }
    pci_set_drvdata(pdev, dev);

    // 4. 映射 BAR0
    dev->bar0_phys = pci_resource_start(pdev, 0);
    dev->bar0_len  = pci_resource_len(pdev, 0);
    dev->bar0_virt = ioremap(dev->bar0_phys, dev->bar0_len);
    if (!dev->bar0_virt) {
        ret = -ENOMEM;
        goto err_free;
    }

    // 5. 分配 MSI-X 中断向量(后续章节详解)
    // ...

    // 6. 注册字符设备 / 网络设备等
    // ...

    return 0;

err_free:
    kfree(dev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

4.3.2 remove 回调:设备清理

remove 回调在设备被拔出、驱动卸载或系统关机时调用。它必须逆序释放 probe 中分配的所有资源:

  1. 注销设备(字符设备、网络设备等)。
  2. 释放中断向量(pci_free_irq_vectors())。
  3. 取消 I/O 映射(iounmap())。
  4. 释放私有数据结构(kfree())。
  5. 禁用 PCI 设备(pci_disable_device())。
static void my_pci_remove(struct pci_dev *pdev)
{
    struct my_dev *dev = pci_get_drvdata(pdev);

    // 1. 注销设备
    // device_destroy(...);
    // class_destroy(...);

    // 2. 释放中断
    pci_free_irq_vectors(pdev);

    // 3. 取消 I/O 映射
    iounmap(dev->bar0_virt);

    // 4. 释放私有数据
    kfree(dev);

    // 5. 禁用 PCI 设备
    pci_disable_device(pdev);
}
最佳实践: 在 probe 中使用 goto 错误处理模式,确保任何一步失败时都能正确回滚已分配的资源。remove 中的清理顺序应与 probe 完全相反。
probe / remove 回调生命周期 probe() 入口 pci_enable_device() dma_set_mask() ioremap() BAR 空间 pci_alloc_irq_vectors() remove() 入口 pci_free_irq_vectors() iounmap() kfree() 私有数据 pci_disable_device() 资源分配与释放 probe 分配 → remove 释放 顺序必须完全相反

4.4 本章小结

  • pci_dev 是内核中 PCI 设备的软件抽象,驱动通过它获取 BAR 资源、中断号和设备标识。
  • pci_driver 通过 pci_register_driver() 注册,内核根据 id_table 自动匹配设备并调用 probe。
  • probe 回调负责设备初始化,包括启用设备、设置 DMA、映射 BAR、分配中断和注册子系统。
  • remove 回调负责逆序清理所有资源,确保设备拔出或驱动卸载时系统状态一致。