4. Linux PCI 子系统:pci_driver 结构、pci_device_id 匹配、probe/remove 回调
在 Linux 内核中,PCI 子系统为驱动开发者提供了标准化的设备-驱动模型。任何 PCIe 端点或桥设备要正常工作,都必须通过 pci_driver 结构体向内核注册,并利用 pci_device_id 表完成设备与驱动的匹配。匹配成功后,内核自动调用 probe 回调;设备移除时调用 remove 回调。理解这三个核心要素,是编写稳定、高性能 PCIe 驱动的基础。
4.1 pci_driver 结构体
pci_driver 是 PCI 设备驱动的核心描述符,定义在 <linux/pci.h> 中。它封装了驱动名称、支持的设备 ID 列表、以及生命周期回调函数。
| 成员 | 类型 | 说明 |
|---|---|---|
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 *) |
电源管理恢复回调(可选) |
shutdown |
void (*shutdown)(struct pci_dev *) |
系统关机时的回调(可选) |
driver |
struct device_driver |
内嵌通用设备驱动结构体 |
关键点:
probe 和 remove 是必须实现的回调。如果驱动不需要电源管理,suspend/resume 可以设为 NULL。
4.2 pci_device_id 匹配机制
pci_device_id 结构体定义了驱动所关心的设备特征。内核 PCI 核心在枚举总线时,会遍历每个设备的配置空间,提取 Vendor ID、Device ID 等信息,并与所有已注册驱动的 id_table 进行匹配。
struct pci_device_id {
__u32 vendor, device; /* 厂商ID、设备ID */
__u32 subvendor, subdevice;/* 子系统厂商ID、子系统设备ID */
__u32 class, class_mask; /* 类别码及掩码 */
unsigned long driver_data; /* 驱动私有数据 */
};
匹配优先级规则如下:
- 精确匹配: vendor + device + subvendor + subdevice 全部匹配。
- 通配匹配: 使用
PCI_ANY_ID(值为0xFFFF)表示任意值。 - 类别匹配: 通过
class和class_mask匹配设备类别。 - 匹配顺序: 内核按
id_table数组顺序依次匹配,返回第一个成功的条目。
最佳实践: 对于自定义 PCIe 设备,建议使用精确匹配(vendor + device + subvendor + subdevice),避免与其他设备冲突。对于通用类驱动(如 NVMe、XHCI),使用类别匹配更合适。
4.3 probe 回调:设备初始化
当 pci_device_id 匹配成功后,内核调用驱动的 probe 函数。这是驱动获取设备资源、建立 DMA 映射、注册中断、创建内核数据结构的入口。
典型的 probe 流程:
- 启用设备: 调用
pci_enable_device()激活 PCI 设备。 - 请求 MMIO/BAR 资源: 使用
pci_request_regions()或pcim_iomap_regions()获取 BAR 地址空间。 - 设置 DMA 掩码: 调用
dma_set_mask_and_coherent()告知设备支持的 DMA 地址宽度。 - 分配中断: 使用
pci_alloc_irq_vectors()分配 MSI/MSI-X 或传统 INTx 中断。 - 初始化硬件: 通过 MMIO 读写配置设备内部寄存器。
- 创建数据结构: 分配私有数据结构,初始化锁、工作队列、DMA 缓冲池等。
- 注册设备: 调用
pci_set_drvdata()保存私有数据指针,注册 misc 或字符设备。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
ret = pcim_enable_device(pdev); /* 自动资源管理 */
if (ret)
return ret;
ret = pcim_iomap_regions(pdev, BIT(0), "my_driver");
if (ret)
return ret;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->bar0 = pcim_iomap_table(pdev)[0];
dev->pdev = pdev;
pci_set_drvdata(pdev, dev);
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
return ret;
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (ret < 0)
return ret;
dev->irq = pci_irq_vector(pdev, 0);
ret = devm_request_irq(&pdev->dev, dev->irq, my_isr, 0, "my_driver", dev);
if (ret)
return ret;
/* 注册字符设备或 misc 设备 */
return 0;
}
4.4 remove 回调:设备清理
remove 回调在设备热拔出、驱动卸载或系统关机时被调用。它的职责是 probe 的逆操作,确保所有资源被正确释放,避免内存泄漏或中断悬空。
典型的 remove 流程:
- 停止所有正在进行的 DMA 传输和硬件操作。
- 注销中断处理函数(使用
devm_系列 API 可自动完成)。 - 释放 DMA 缓冲区和环形队列。
- 注销字符设备或 misc 设备节点。
- 内核自动释放
devm_管理的资源(MMIO 映射、内存、中断)。
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_dev *dev = pci_get_drvdata(pdev);
/* 停止硬件操作 */
writel(0, dev->bar0 + REG_CTRL);
/* 注销设备节点(如果手动注册) */
/* misc_deregister(&dev->miscdev); */
/* devm_ 系列 API 会自动释放资源,无需手动调用 */
}
注意: 如果
probe 中使用了非 devm_ 的 API(如 pci_enable_device 而非 pcim_enable_device),则必须在 remove 中手动调用 pci_disable_device()、pci_release_regions() 等逆操作。推荐优先使用 pcim_ 和 devm_ 系列 API 简化资源管理。
4.5 驱动注册与注销
驱动模块的入口和出口函数负责向内核注册 pci_driver:
static struct pci_driver my_pci_driver = {
.name = "my_pcie_driver",
.id_table = my_pci_ids,
.probe = my_pci_probe,
.remove = my_pci_remove,
};
static int __init my_driver_init(void)
{
return pci_register_driver(&my_pci_driver);
}
static void __exit my_driver_exit(void)
{
pci_unregister_driver(&my_pci_driver);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
4.6 设备-驱动匹配与生命周期 SVG 图
下图展示了从 PCI 枚举到驱动 probe/remove 的完整生命周期:
4.7 关键数据结构关系图
下图展示了 pci_driver、pci_device_id、pci_dev 之间的核心关系:
4.8 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| probe 未被调用 | id_table 未匹配或驱动未注册 | 检查 vendor/device ID 是否正确;确认 pci_register_driver 成功 |
| probe 返回 -ENODEV | 设备资源不足或硬件初始化失败 | 检查 BAR 是否已分配;确认 DMA 掩码设置正确 |
| remove 后设备仍可访问 | 未正确停止硬件 DMA 或未注销中断 | 在 remove 中先写寄存器停止硬件,再释放资源 |
| 多个设备共用一个驱动 | id_table 包含多个条目 | 利用 driver_data 区分不同设备型号的行为 |
| 热拔出导致内核崩溃 | remove 回调未正确处理正在进行的 I/O | 使用引用计数或 completion 机制确保所有 I/O 完成后再释放 |
调试技巧: 在
probe 和 remove 中添加 dev_info() 打印,配合 lspci -vvv 和 cat /sys/bus/pci/devices/.../driver 确认驱动绑定状态。