4. Linux PCI子系统

在 Linux 内核中,PCI 子系统为设备驱动开发者提供了一套标准化的接口框架。任何 PCIe 设备驱动的核心工作,都是围绕 pci_driver 结构体、pci_device_id 匹配表以及 probe/remove 回调函数展开的。本章将深入剖析这三个核心要素,帮助你理解驱动与内核 PCI 核心层的协作机制。

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

pci_driver 是 PCI 设备驱动在内核中的注册实体,它包含了驱动的名称、支持的设备列表、以及关键的探测与移除回调函数。驱动通过 pci_register_driver() 向内核注册此结构体,从而告知 PCI 核心:“我能管理这些设备”。

关键字段解析:
  • name:驱动名称,用于在 /sys/bus/pci/drivers/ 下创建目录。
  • id_table:指向 pci_device_id 数组的指针,定义该驱动支持的设备。
  • probe:当 PCI 核心发现匹配设备时调用的探测函数,驱动在此完成硬件初始化。
  • remove:设备被移除或驱动卸载时调用的清理函数。
  • suspend / resume:电源管理回调,用于系统休眠/唤醒场景。
// 一个典型的 pci_driver 定义示例
static struct pci_driver my_pci_driver = {
    .name     = "my_pcie_device",
    .id_table = my_pci_ids,          // 指向设备匹配表
    .probe    = my_pci_probe,        // 探测回调
    .remove   = my_pci_remove,       // 移除回调
    .driver   = {
        .pm = &my_pci_pm_ops,       // 电源管理操作(可选)
    },
};

// 在模块初始化函数中注册
static int __init my_driver_init(void)
{
    return pci_register_driver(&my_pci_driver);
}
module_init(my_driver_init);

// 在模块退出函数中注销
static void __exit my_driver_exit(void)
{
    pci_unregister_driver(&my_pci_driver);
}
module_exit(my_driver_exit);
💡 最佳实践: 始终在 probe 中检查 pci_enable_device() 的返回值,并在 remove 中调用 pci_disable_device() 以确保设备电源状态正确。

4.2 pci_device_id 匹配表:驱动与设备的“桥梁”

pci_device_id 结构体定义了驱动能够识别的 PCI 设备特征。内核 PCI 核心在枚举总线时,会遍历所有已注册驱动的 id_table,寻找与设备配置空间中的 Vendor ID、Device ID 等字段匹配的条目。一旦匹配成功,内核便会调用该驱动的 probe 函数。

字段 描述 匹配规则
vendor 厂商 ID(如 0x8086 为 Intel) 必须完全匹配,除非使用 PCI_ANY_ID
device 设备 ID(如 0x10fb) 必须完全匹配,除非使用 PCI_ANY_ID
subvendor 子系统厂商 ID 可选匹配,通常设为 PCI_ANY_ID
subdevice 子系统设备 ID 可选匹配,通常设为 PCI_ANY_ID
class 设备类别(如 0x020000 为以太网控制器) 按类别匹配,用于通用驱动
class_mask 类别掩码,决定 class 的哪些位参与匹配 与 class 配合使用
driver_data 驱动私有数据,可传递设备特定信息 匹配成功后通过 pci_match_id() 获取
// 设备匹配表示例:支持两种不同型号的 PCIe 设备
#define MY_VENDOR_ID1  0x1234
#define MY_DEVICE_ID1  0x5678
#define MY_VENDOR_ID2  0x1234
#define MY_DEVICE_ID2  0x9abc

static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(MY_VENDOR_ID1, MY_DEVICE_ID1) },
    { PCI_DEVICE(MY_VENDOR_ID2, MY_DEVICE_ID2) },
    { 0, }  // 必须以空条目结束
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);  // 导出设备表,支持热插拔
⚠️ 注意事项: 匹配表必须以 { 0, } 结尾,否则内核在遍历时会越界访问导致崩溃。MODULE_DEVICE_TABLE 宏用于将设备表导出到用户空间,是 modprobe 自动加载驱动的关键。
pci_device_id 匹配流程 PCI 核心枚举设备 读取 Vendor/Device ID 遍历驱动 id_table 逐条比较 pci_device_id 匹配成功 调用 probe 回调 驱动开始初始化硬件 匹配失败 尝试下一个驱动 返回枚举 📌 匹配优先级: 1. 精确匹配 vendor + device → 2. 子系统匹配 → 3. 类别匹配 (class) → 4. PCI_ANY_ID 通配 匹配成功后,内核将 pci_dev 指针传递给 probe 函数,驱动可立即访问设备资源。

4.3 probe 回调:驱动的“初始化战场”

probe 函数是驱动与硬件第一次正式交互的入口。当 PCI 核心找到匹配的设备后,会调用驱动的 probe 函数,并传入一个 struct pci_dev * 指针和一个 const struct pci_device_id * 指针。驱动在此函数中完成以下关键步骤:

  1. 使能设备:调用 pci_enable_device() 激活 PCI 配置空间和 I/O 资源。
  2. 请求资源:调用 pci_request_regions() 声明对 BAR 空间的独占访问。
  3. 设置 DMA 掩码:调用 dma_set_mask() 和 dma_set_coherent_mask() 配置设备 DMA 地址宽度。
  4. 映射 BAR 空间:使用 pci_iomap() 将 BAR 物理地址映射到内核虚拟地址空间。
  5. 注册中断:调用 request_irq() 或 pci_alloc_irq_vectors() 注册中断处理函数。
  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. 请求 I/O 和内存资源
    ret = pci_request_regions(pdev, "my_pcie_device");
    if (ret)
        goto err_disable;

    // 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_release;
    }

    // 4. 映射 BAR0(通常为内存空间)
    void __iomem *bar0 = pci_iomap(pdev, 0, 0);
    if (!bar0) {
        ret = -ENOMEM;
        goto err_release;
    }

    // 5. 分配私有数据结构体
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        ret = -ENOMEM;
        goto err_unmap;
    }
    dev->pdev = pdev;
    dev->bar0 = bar0;
    dev->driver_data = id->driver_data;  // 保存匹配表中的私有数据

    // 6. 注册中断(MSI-X 优先)
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY);
    if (ret < 0)
        goto err_unmap;
    dev->irq = pci_irq_vector(pdev, 0);
    ret = request_irq(dev->irq, my_interrupt_handler, 0, "my_pcie", dev);
    if (ret)
        goto err_free_vectors;

    pci_set_drvdata(pdev, dev);  // 将私有数据关联到 pci_dev
    return 0;

err_free_vectors:
    pci_free_irq_vectors(pdev);
err_unmap:
    pci_iounmap(pdev, bar0);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
💡 调试技巧: 在 probe 函数中可以使用 dev_info(&pdev->dev, "probe success\n") 输出日志,通过 dmesg 观察驱动加载过程。如果 probe 返回非零值,内核会认为驱动无法管理该设备。

4.4 remove 回调:驱动的“善后工作”

remove 函数是 probe 的逆操作,在设备被拔出、驱动卸载或系统关闭时调用。它的职责是释放所有在 probe 中申请的资源,确保系统状态干净。

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

    // 1. 释放中断
    free_irq(dev->irq, dev);
    pci_free_irq_vectors(pdev);

    // 2. 取消 BAR 映射
    pci_iounmap(pdev, dev->bar0);

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

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

    // 私有数据由 devm_kzalloc 自动释放,无需手动 free
}
⚠️ 常见错误: 忘记在 remove 中释放中断或取消映射,会导致内核在卸载驱动时产生 BUG: unable to handle kernel paging request 等严重错误。务必保证 probe 和 remove 的资源操作完全对称。
probe / remove 生命周期与资源对称 probe 开始 pci_enable_device() pci_request_regions() pci_iomap() + request_irq() 时间 remove 开始 free_irq() + pci_iounmap() pci_release_regions() pci_disable_device() 对称释放 对称释放 对称释放 🔁 核心原则:remove 中资源的释放顺序必须与 probe 中的申请顺序完全相反(后进先出)。

4.5 完整驱动框架模板

将上述三个要素组合起来,就构成了一个完整的 PCIe 设备驱动骨架。以下模板展示了从注册到注销的完整流程:

// pci_driver 结构体定义
static struct pci_driver my_pci_driver = {
    .name     = "my_pcie_device",
    .id_table = my_pci_ids,
    .probe    = my_pci_probe,
    .remove   = my_pci_remove,
};

// 模块入口/出口
module_pci_driver(my_pci_driver);  // 简化写法,等价于 module_init/exit + pci_register/unregister

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("PCIe Device Driver Example");
📋 关键检查清单:
  • ✅ pci_device_id 表以 { 0, } 结尾
  • ✅ 使用 MODULE_DEVICE_TABLE 导出设备表
  • ✅ probe 中检查所有关键函数的返回值
  • ✅ remove 中按逆序释放所有资源
  • ✅ 使用 devm_* 系列函数简化资源管理(如 devm_kzalloc、devm_request_irq)
  • ✅ 调用 pci_set_drvdata 保存私有数据,便于 remove 中访问