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回调在其中扮演的角色。
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,使代码更加简洁。这是现代内核驱动推荐的做法。