3. Linux PCI子系统架构
本章深入剖析 Linux PCI 子系统为设备驱动开发者提供的核心框架。理解 pci_driver 结构体、pci_device_id 匹配表以及模块加载/卸载的完整生命周期,是编写任何 PCI 设备驱动的基石。我们将从数据结构定义出发,逐步拆解内核如何发现设备、匹配驱动并完成初始化。
3.1 pci_driver 结构体:驱动的“身份证”
在 Linux 内核中,每个 PCI 驱动都必须实例化一个 struct pci_driver 结构体。这个结构体是驱动与 PCI 核心之间的契约,它定义了驱动的身份、支持哪些设备以及关键的操作函数指针。
| 成员名称 | 类型 | 描述 |
|---|---|---|
name |
const char * |
驱动名称,用于 sysfs 和内核日志标识 |
id_table |
const struct pci_device_id * |
指向设备 ID 匹配表的指针,驱动支持的设备列表 |
probe |
int (*probe)(struct pci_dev *, const struct pci_device_id *) |
当设备与驱动匹配成功时调用,执行设备初始化 |
remove |
void (*remove)(struct pci_dev *) |
设备被移除或驱动卸载时调用,执行资源清理 |
suspend |
int (*suspend)(struct pci_dev *, pm_message_t) |
电源管理:挂起设备(可选) |
resume |
int (*resume)(struct pci_dev *) |
电源管理:恢复设备(可选) |
driver |
struct device_driver |
内嵌的通用设备驱动结构体,由 PCI 核心管理 |
关键点:
probe 和 remove 是驱动必须实现的两个核心回调。probe 函数是驱动获取硬件控制权的入口,通常在这里完成 I/O 端口/内存映射、DMA 缓冲区分配、中断注册等操作。
以下是一个典型的 pci_driver 实例化示例:
#include <linux/pci.h>
#include <linux/module.h>
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
pr_info("MyPCI: Device found (vendor=0x%04x, device=0x%04x)\n", id->vendor, id->device);
// 在此处进行硬件初始化
return 0;
}
static void my_pci_remove(struct pci_dev *pdev) {
pr_info("MyPCI: Device removed\n");
// 在此处进行资源清理
}
static struct pci_driver my_pci_driver = {
.name = "my_pci_driver",
.id_table = my_pci_id_table, // 指向匹配表
.probe = my_pci_probe,
.remove = my_pci_remove,
};
module_pci_driver(my_pci_driver);
3.2 pci_device_id 匹配表:驱动与设备的“红娘”
pci_device_id 结构体定义了驱动所支持的 PCI 设备特征。PCI 核心通过遍历驱动的 ID 表,与系统中每个 PCI 设备的配置空间信息进行比对,从而决定是否调用该驱动的 probe 函数。
struct pci_device_id {
__u32 vendor, device; // 厂商ID和设备ID
__u32 subvendor, subdevice; // 子系统厂商ID和设备ID
__u32 class, class_mask; // 设备类别和掩码
unsigned long driver_data; // 驱动私有数据,可传递给 probe
};
匹配优先级: 内核按照 ID 表在数组中的顺序进行匹配。通常将最具体的匹配(指定了 vendor + device + subvendor + subdevice)放在前面,将通配符匹配放在后面。
匹配表通常使用 MODULE_DEVICE_TABLE 宏导出,以便内核在模块加载时知道该驱动支持哪些设备,也用于 depmod 工具生成模块依赖关系。
#define MY_VENDOR_ID 0x10EE // 示例厂商ID
#define MY_DEVICE_ID 0x1234 // 示例设备ID
static const struct pci_device_id my_pci_id_table[] = {
{ PCI_DEVICE(MY_VENDOR_ID, MY_DEVICE_ID) },
{ PCI_DEVICE(0x8086, 0x100E) }, // 支持 Intel 千兆网卡
{ 0, } // 必须以空条目结束
};
MODULE_DEVICE_TABLE(pci, my_pci_id_table);
常用的匹配宏:
PCI_DEVICE(vendor, device):匹配指定的厂商 ID 和设备 ID。PCI_DEVICE_CLASS(class, class_mask):匹配指定的设备类别。PCI_VDEVICE(vendor, device):使用厂商名称字符串(如PCI_VENDOR_ID_INTEL)简化代码。
3.3 模块加载与卸载流程:驱动的生命周期
PCI 驱动的加载和卸载遵循标准的 Linux 内核模块机制,但 PCI 核心在其中扮演了关键的协调角色。整个流程可以概括为“注册-匹配-初始化”和“反初始化-注销”。
3.3.1 模块加载流程
- 模块入口:当执行
insmod或内核自动加载模块时,调用模块初始化函数(由module_init指定)。 - 注册驱动:驱动调用
pci_register_driver(),将pci_driver结构体传递给 PCI 核心。 - 设备枚举:PCI 核心遍历系统中所有已发现的 PCI 设备,对每个设备检查驱动的
id_table。 - 匹配与 probe:如果找到匹配项,PCI 核心调用驱动的
probe函数,并传入对应的pci_dev和pci_device_id指针。 - 设备初始化:在
probe函数中,驱动完成硬件初始化(如启用设备、分配 I/O 内存、注册中断等)。
3.3.2 模块卸载流程
- 模块退出:执行
rmmod时,调用模块退出函数(由module_exit指定)。 - 注销驱动:驱动调用
pci_unregister_driver(),通知 PCI 核心该驱动即将卸载。 - 调用 remove:PCI 核心为每个与该驱动关联的设备调用驱动的
remove函数。 - 资源清理:在
remove函数中,驱动释放所有已分配的资源(I/O 内存、DMA 缓冲区、中断等)。 - 驱动注销:PCI 核心将驱动从内部链表中移除,模块安全卸载。
常见陷阱: 在
probe 中分配的资源必须在 remove 中完全释放,否则会导致内存泄漏或内核崩溃。此外,probe 函数可能被并发调用(SMP 系统),必须考虑竞态条件。
3.4 辅助宏与最佳实践
为了简化代码,内核提供了 module_pci_driver() 宏,它自动生成标准的模块加载和卸载函数。对于大多数驱动,只需在文件末尾调用此宏即可。
/* 使用 module_pci_driver 宏简化代码 */
module_pci_driver(my_pci_driver);
/* 等价于以下手动展开的代码:
static int __init my_pci_init(void) {
return pci_register_driver(&my_pci_driver);
}
static void __exit my_pci_exit(void) {
pci_unregister_driver(&my_pci_driver);
}
module_init(my_pci_init);
module_exit(my_pci_exit);
*/
最佳实践清单:
- 始终使用
module_pci_driver()宏,除非需要自定义初始化/退出逻辑。 - ID 表必须以
{ 0, }结尾,并使用MODULE_DEVICE_TABLE导出。 - 在
probe中启用 PCI 设备:pci_enable_device(pdev)。 - 在
remove中禁用 PCI 设备:pci_disable_device(pdev)。 - 使用
pci_set_drvdata(pdev, data)和pci_get_drvdata(pdev)在 probe/remove 之间传递私有数据。