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)简化代码。
pci_device_id 匹配流程 PCI 核心 设备 ID 表 pci_device_id[] 匹配成功 → probe() 遍历 id_table,比对 vendor/device/class 说明:PCI 核心在总线枚举或热插拔时,遍历每个设备的 ID 表,找到第一个匹配项后调用 probe()

3.3 模块加载与卸载流程:驱动的生命周期

PCI 驱动的加载和卸载遵循标准的 Linux 内核模块机制,但 PCI 核心在其中扮演了关键的协调角色。整个流程可以概括为“注册-匹配-初始化”和“反初始化-注销”。

3.3.1 模块加载流程

  1. 模块入口:当执行 insmod 或内核自动加载模块时,调用模块初始化函数(由 module_init 指定)。
  2. 注册驱动:驱动调用 pci_register_driver(),将 pci_driver 结构体传递给 PCI 核心。
  3. 设备枚举:PCI 核心遍历系统中所有已发现的 PCI 设备,对每个设备检查驱动的 id_table。
  4. 匹配与 probe:如果找到匹配项,PCI 核心调用驱动的 probe 函数,并传入对应的 pci_dev 和 pci_device_id 指针。
  5. 设备初始化:在 probe 函数中,驱动完成硬件初始化(如启用设备、分配 I/O 内存、注册中断等)。

3.3.2 模块卸载流程

  1. 模块退出:执行 rmmod 时,调用模块退出函数(由 module_exit 指定)。
  2. 注销驱动:驱动调用 pci_unregister_driver(),通知 PCI 核心该驱动即将卸载。
  3. 调用 remove:PCI 核心为每个与该驱动关联的设备调用驱动的 remove 函数。
  4. 资源清理:在 remove 函数中,驱动释放所有已分配的资源(I/O 内存、DMA 缓冲区、中断等)。
  5. 驱动注销:PCI 核心将驱动从内部链表中移除,模块安全卸载。
常见陷阱: 在 probe 中分配的资源必须在 remove 中完全释放,否则会导致内存泄漏或内核崩溃。此外,probe 函数可能被并发调用(SMP 系统),必须考虑竞态条件。
PCI 驱动模块加载与卸载流程 加载流程 1. module_init() 2. pci_register_driver() 3. 遍历设备 ID 表 4. 匹配成功 → probe() 5. 设备初始化完成 卸载流程 1. module_exit() 2. pci_unregister_driver() 3. 遍历关联设备 4. 调用 remove() 5. 资源清理完成 提示:module_pci_driver() 宏封装了 module_init/module_exit 和 pci_register_driver/pci_unregister_driver 的样板代码

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 之间传递私有数据。