2. Linux PCI子系统架构:pci_driver结构体、pci_device_id匹配表、模块加载与卸载流程
Linux PCI子系统是内核中设备模型与总线驱动框架的典范实现。理解其核心数据结构与生命周期管理,是编写稳定、可移植PCIe驱动的基石。本节将深入剖析pci_driver结构体、pci_device_id匹配表以及模块加载/卸载的标准流程。
2.1 pci_driver结构体:驱动核心
struct pci_driver是PCI设备驱动的“身份证”与“操作手册”,它向内核声明驱动的能力、支持的设备以及回调函数。定义于<linux/pci.h>。
| 成员 | 类型 | 说明 |
|---|---|---|
name |
const char * |
驱动名称,用于/sys/bus/pci/drivers/目录标识 |
id_table |
const struct pci_device_id * |
指向设备ID匹配表,决定驱动支持哪些PCI设备 |
probe |
int (*)(struct pci_dev *, const struct pci_device_id *) |
设备发现时调用,完成硬件初始化、资源分配、注册中断等 |
remove |
void (*)(struct pci_dev *) |
设备移除或驱动卸载时调用,释放资源、注销中断 |
suspend |
int (*)(struct pci_dev *, pm_message_t) |
电源管理:挂起设备(可选) |
resume |
int (*)(struct pci_dev *) |
电源管理:恢复设备(可选) |
shutdown |
void (*)(struct pci_dev *) |
系统关机时调用(可选) |
driver |
struct device_driver |
内嵌通用设备驱动结构体,用于设备模型框架 |
关键点:
probe 和 remove 是必须实现的两个回调。驱动的大部分初始化逻辑(如使能DMA、映射BAR空间、申请中断)都在 probe 中完成,而 remove 负责逆序清理。
2.2 pci_device_id匹配表:驱动与设备的桥梁
内核通过pci_device_id表判断哪个驱动可以接管某个PCI设备。匹配字段包括Vendor ID、Device ID、Subsystem Vendor ID、Subsystem Device ID以及Class等。
/* pci_device_id 结构体定义 (简化) */
struct pci_device_id {
__u32 vendor; /* 厂商ID,PCI_VENDOR_ID_xxx 或 PCI_ANY_ID */
__u32 device; /* 设备ID,PCI_DEVICE_ID_xxx 或 PCI_ANY_ID */
__u32 subvendor; /* 子系统厂商ID,通常 PCI_ANY_ID */
__u32 subdevice; /* 子系统设备ID,通常 PCI_ANY_ID */
__u32 class; /* 设备类别 */
__u32 class_mask; /* 类别掩码 */
kernel_ulong_t driver_data; /* 驱动私有数据,可传递设备特定信息 */
};
匹配技巧: 使用
PCI_ANY_ID (值为0xFFFF) 可以匹配任意厂商或设备ID,但应谨慎使用,避免驱动错误绑定不兼容硬件。推荐精确匹配 Vendor ID + Device ID。
匹配表必须以MODULE_DEVICE_TABLE(pci, my_id_table)宏导出,以便内核模块工具(如modprobe)生成别名,实现自动加载。
/* 示例:匹配表定义 */
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x7014) }, /* 精确匹配厂商0x10EE, 设备0x7014 */
{ PCI_DEVICE_SUB(0x10EE, 0x7015, 0x10EE, 0x0001) }, /* 匹配子系统 */
{ PCI_DEVICE_CLASS(0x020000, 0xFFFFFF) }, /* 匹配以太网控制器类 */
{ 0, } /* 终止条目 */
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
2.3 模块加载与卸载流程
PCI驱动通常以内核模块形式存在。加载与卸载流程涉及模块入口/出口函数、PCI子系统注册以及设备生命周期回调。
2.3.1 标准流程步骤
- 模块加载:
module_init(my_init)调用驱动初始化函数。 - 注册驱动:调用
pci_register_driver(&my_driver),将驱动加入PCI总线。 - 设备匹配:内核遍历已枚举的PCI设备,与驱动的
id_table进行匹配。 - probe回调:匹配成功,调用驱动的
probe函数,完成硬件初始化。 - 设备运行:驱动正常工作,处理I/O、中断、DMA等。
- 模块卸载:
module_exit(my_exit)调用驱动清理函数。 - 注销驱动:调用
pci_unregister_driver(&my_driver),触发所有已绑定设备的remove回调。 - 资源释放:
remove回调释放硬件资源、注销中断、禁用DMA等。
常见错误: 在
probe 中分配资源后,如果后续步骤失败,必须回滚已分配的所有资源,否则卸载模块时会导致内存泄漏或内核崩溃。推荐使用 goto 错误处理模式。
2.3.2 完整代码模板
#include <linux/module.h>
#include <linux/pci.h>
/* 设备ID表 */
static const struct pci_device_id my_pci_ids[] = {
{ PCI_DEVICE(0x10EE, 0x7014) },
{ }
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);
/* probe 回调 */
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
pr_info("my_pci: probe device %04x:%04x\n", pdev->vendor, pdev->device);
/* 1. 使能PCI设备 */
ret = pci_enable_device(pdev);
if (ret)
return ret;
/* 2. 请求BAR资源 */
ret = pci_request_regions(pdev, "my_pci");
if (ret)
goto err_disable;
/* 3. 设置DMA掩码 (后续章节详解) */
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
goto err_release;
/* 4. 申请中断 (后续章节详解) */
/* ... */
pci_set_drvdata(pdev, /* 私有数据结构指针 */);
return 0;
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
/* remove 回调 */
static void my_pci_remove(struct pci_dev *pdev)
{
/* 逆序释放probe中分配的资源 */
/* 注销中断 */
/* 释放私有数据 */
pci_release_regions(pdev);
pci_disable_device(pdev);
pr_info("my_pci: device removed\n");
}
/* pci_driver 结构体 */
static struct pci_driver my_pci_driver = {
.name = "my_pci_driver",
.id_table = my_pci_ids,
.probe = my_pci_probe,
.remove = my_pci_remove,
};
/* 模块初始化 */
static int __init my_init(void)
{
return pci_register_driver(&my_pci_driver);
}
/* 模块退出 */
static void __exit my_exit(void)
{
pci_unregister_driver(&my_pci_driver);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux PCIe Driver Developer");
MODULE_DESCRIPTION("Sample PCI driver for subsystem architecture demo");
2.4 PCI子系统架构与驱动生命周期SVG图
下图展示了PCI子系统核心组件、数据结构关系以及驱动从加载到卸载的完整生命周期。
2.5 关键避坑指南
| 场景 | 常见错误 | 正确做法 |
|---|---|---|
| probe失败 | 直接返回错误,未释放已使能的设备 | 使用 goto 模式逐级回滚 |
| id_table未导出 | 忘记 MODULE_DEVICE_TABLE,导致无法自动加载 |
始终在id_table定义后添加宏导出 |
| remove遗漏清理 | 只注销中断,未释放BAR或禁用设备 | 严格按照probe的逆序清理所有资源 |
| 并发访问 | probe/remove与中断处理函数竞态 | 使用自旋锁或互斥锁保护共享数据 |
调试建议: 在
probe 和 remove 中添加 dev_info() 或 dev_dbg() 打印,配合 dmesg 观察驱动生命周期是否按预期执行。使用 lspci -k 可查看当前设备绑定的驱动。