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 自动加载驱动的关键。
4.3 probe 回调:驱动的“初始化战场”
probe 函数是驱动与硬件第一次正式交互的入口。当 PCI 核心找到匹配的设备后,会调用驱动的 probe 函数,并传入一个 struct pci_dev * 指针和一个 const struct pci_device_id * 指针。驱动在此函数中完成以下关键步骤:
- 使能设备:调用
pci_enable_device()激活 PCI 配置空间和 I/O 资源。 - 请求资源:调用
pci_request_regions()声明对 BAR 空间的独占访问。 - 设置 DMA 掩码:调用
dma_set_mask()和dma_set_coherent_mask()配置设备 DMA 地址宽度。 - 映射 BAR 空间:使用
pci_iomap()将 BAR 物理地址映射到内核虚拟地址空间。 - 注册中断:调用
request_irq()或pci_alloc_irq_vectors()注册中断处理函数。 - 初始化设备私有数据:分配并初始化驱动自定义的结构体,保存映射地址、中断号等关键信息。
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 的资源操作完全对称。
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中访问