2. Linux PCI子系统框架:pci_driver结构体、probe/remove回调、设备ID表匹配机制
在Linux内核中,PCI子系统为设备驱动开发者提供了高度抽象化的框架。编写一个PCIe设备驱动的核心工作,就是填充一个 pci_driver 结构体,并注册到内核。本章将深入剖析这个框架的三个关键要素:驱动核心结构体、生命周期回调函数以及设备匹配机制。
核心要点: PCI驱动模型基于“设备-驱动”分离的架构。内核通过设备ID表自动匹配硬件设备,并调用驱动的
probe 回调完成初始化;当设备移除或驱动卸载时,调用 remove 回调进行清理。
2.1 pci_driver结构体:驱动的“身份证”
pci_driver 是PCI驱动与内核之间的契约。驱动必须定义并初始化该结构体的关键字段,然后通过 pci_register_driver() 将其注册到PCI核心。
以下是 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);
void (*shutdown)(struct pci_dev *dev);
struct device_driver driver; // 内嵌通用驱动结构
// ... 其他高级字段(如SR-IOV、错误处理等)
};
| 字段 | 类型 | 作用 |
|---|---|---|
name |
const char * |
驱动名称,在 /sys/bus/pci/drivers/ 下可见 |
id_table |
const struct pci_device_id * |
指向设备ID数组的指针,决定驱动支持哪些硬件 |
probe |
回调函数指针 | 设备匹配成功时调用,负责初始化设备、分配资源、注册DMA缓冲区等 |
remove |
回调函数指针 | 设备移除或驱动卸载时调用,负责释放资源、停止DMA传输 |
suspend / resume |
回调函数指针 | 电源管理回调,用于系统休眠/唤醒场景 |
最佳实践: 在
probe 中完成所有资源分配(包括DMA缓冲区和环形缓冲区),在 remove 中按相反顺序释放。这能有效避免资源泄漏。
2.2 probe/remove回调:驱动的生命周期
probe 和 remove 是驱动中最重要的两个回调函数,它们定义了设备从“被发现”到“被移除”的完整生命周期。
2.2.1 probe回调:设备初始化入口
当PCI核心发现一个设备的Vendor ID、Device ID与驱动的 id_table 匹配时,会调用驱动的 probe 函数。典型的 probe 流程包括:
- 启用PCI设备: 调用
pci_enable_device()激活设备。 - 设置DMA掩码: 调用
dma_set_mask()和dma_set_coherent_mask()告知设备支持的地址宽度(如64位)。 - 请求MMIO/BAR资源: 调用
pci_request_regions()获取BAR空间。 - 映射BAR到虚拟地址: 调用
pcim_iomap()或ioremap()将物理地址映射到内核虚拟地址空间。 - 设置DMA传输引擎: 初始化环形缓冲区、分配DMA缓冲池、注册中断处理函数。
- 注册设备: 调用
pci_set_drvdata()保存私有数据结构指针。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_device *mydev;
int ret;
// 1. 分配私有数据结构
mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
if (!mydev)
return -ENOMEM;
// 2. 启用PCI设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 3. 设置64位DMA掩码
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
dev_err(&pdev->dev, "No suitable DMA mask available\n");
goto err_disable;
}
// 4. 请求并映射BAR0
ret = pcim_iomap_regions(pdev, BIT(0), KBUILD_MODNAME);
if (ret)
goto err_disable;
mydev->bar0 = pcim_iomap_table(pdev)[0];
mydev->pdev = pdev;
pci_set_drvdata(pdev, mydev);
// 5. 初始化环形缓冲区与DMA(后续章节详述)
// ret = my_init_ring_buffer(mydev);
// if (ret) goto err_disable;
// 6. 注册中断(MSI-X优先)
// ret = request_irq(...);
dev_info(&pdev->dev, "Device probed successfully\n");
return 0;
err_disable:
pci_disable_device(pdev);
return ret;
}
2.2.2 remove回调:设备清理出口
remove 回调负责撤销 probe 中执行的所有操作。内核保证 remove 在设备热拔插、驱动卸载或系统关闭时被调用。
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_device *mydev = pci_get_drvdata(pdev);
// 1. 释放中断
// free_irq(mydev->irq, mydev);
// 2. 停止DMA传输并释放环形缓冲区
// my_cleanup_ring_buffer(mydev);
// 3. 释放BAR映射(pcim_iomap_regions 自动管理)
// 4. 禁用设备
pci_disable_device(pdev);
// 5. 私有数据由 devm_kzalloc 自动释放
dev_info(&pdev->dev, "Device removed\n");
}
注意: 如果使用了
devm_* 系列API(如 devm_kzalloc、pcim_iomap_regions),内核会在设备分离时自动释放资源。但DMA描述符、环形缓冲区指针等仍需在 remove 中手动清理。
2.3 设备ID表匹配机制
设备ID表是驱动与硬件之间的“桥梁”。内核通过遍历驱动的 id_table,与每个PCI设备的Vendor ID、Device ID、Subsystem ID等字段进行匹配。
2.3.1 pci_device_id结构体
struct pci_device_id {
__u32 vendor, device; // 主 Vendor ID 和 Device ID
__u32 subvendor, subdevice;// Subsystem Vendor ID 和 Device ID
__u32 class, class_mask; // 设备类别与掩码
unsigned long driver_data; // 驱动私有数据(如硬件版本标识)
};
| 字段 | 匹配规则 | 示例值 |
|---|---|---|
vendor |
必须完全匹配,PCI_ANY_ID 表示通配 |
0x10EE (Xilinx) |
device |
必须完全匹配,PCI_ANY_ID 表示通配 |
0x7021 (DMA控制器) |
subvendor / subdevice |
用于区分同一芯片的不同板卡版本 | PCI_ANY_ID |
class / class_mask |
按设备类别匹配(如存储控制器、网络控制器) | 0x020000 (以太网) |
driver_data |
匹配成功后传递给 probe 的私有数据 |
0x01 (硬件版本A) |
2.3.2 匹配优先级与流程
PCI核心的匹配顺序遵循“精确匹配优先”原则:
- 精确匹配: vendor + device + subvendor + subdevice 全部匹配。
- 通配匹配: vendor + device 匹配,subvendor/subdevice 为
PCI_ANY_ID。 - 类别匹配: 通过
class和class_mask进行范围匹配。
匹配示例: 一个Xilinx DMA设备(Vendor=0x10EE, Device=0x7021)会依次检查驱动表中的所有条目,直到找到 vendor=0x10EE 且 device=0x7021 的条目。如果驱动同时定义了 subvendor=0x10EE, subdevice=0x0001,则只有特定版本的板卡才会匹配。
2.3.3 设备ID表定义示例
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x7021), // 匹配所有Xilinx 0x7021设备
.driver_data = MY_HW_V1 },
{ PCI_DEVICE_SUB(0x10EE, 0x7022, // 匹配特定子系统的设备
0x10EE, 0x0002),
.driver_data = MY_HW_V2 },
{ 0, } // 终止条目
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
2.4 整体框架流程图
下图展示了从驱动注册到设备匹配、probe/remove调用的完整流程:
2.5 关键数据结构关系图
以下SVG展示了 pci_driver、pci_device_id、pci_dev 以及内核PCI核心之间的静态关系:
2.6 驱动注册与注销示例
完整的PCI驱动入口点通常使用 module_init / module_exit 宏:
static int __init my_pci_init(void)
{
int ret;
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_pci_exit(void)
{
pci_unregister_driver(&my_pci_driver);
}
module_init(my_pci_init);
module_exit(my_pci_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("PCIe DMA Driver - Ring Buffer Example");
调试技巧: 使用
modinfo my_driver.ko 查看驱动的设备ID表;使用 lspci -v 查看当前系统中PCI设备的Vendor/Device ID,验证匹配是否生效。
2.7 本章小结
- pci_driver 是PCI驱动的核心结构体,必须填充
name、id_table、probe和remove字段。 - probe 回调负责设备初始化,包括启用设备、设置DMA掩码、映射BAR、分配环形缓冲区;remove 回调负责逆序清理。
- 设备ID表 通过
pci_device_id数组定义,支持精确匹配、通配匹配和类别匹配,匹配成功后内核自动调用probe。 - 使用
MODULE_DEVICE_TABLE(pci, ...)导出ID表,使内核在模块加载前即可进行设备匹配。