3. Linux PCIe 子系统架构:核心数据结构与驱动生命周期
在 Linux 内核中,PCIe 子系统为驱动开发者提供了一套标准化的框架。理解 pci_driver 结构体、pci_device_id 匹配表以及 probe/remove 回调函数的生命周期,是编写稳定、高效 PCIe 驱动的基础。本章将深入剖析这三个核心要素,并结合工程案例说明其在实际链路故障排查中的应用。
3.1 pci_driver 结构体:驱动的“身份证”
pci_driver 是 PCIe 驱动与内核之间的契约。每个 PCIe 驱动都必须实例化并注册一个 pci_driver 结构体,内核通过它来识别、匹配和管理驱动。
关键字段解析:
- name:驱动名称,用于 sysfs 和调试。
- id_table:指向
pci_device_id表的指针,定义该驱动支持的设备列表。 - probe:当设备与驱动匹配成功时调用的初始化函数。
- remove:设备被移除或驱动卸载时调用的清理函数。
- shutdown:系统关机时的回调。
- driver.pm:电源管理操作结构体。
// 一个典型的 pci_driver 定义示例
static struct pci_driver my_pcie_driver = {
.name = "my_pcie_device",
.id_table = my_pcie_ids, // 匹配表
.probe = my_pcie_probe, // 探测函数
.remove = my_pcie_remove, // 移除函数
.shutdown = my_pcie_shutdown, // 关机处理
.driver = {
.pm = &my_pcie_pm_ops, // 电源管理
},
};
3.2 pci_device_id 匹配表:驱动与设备的“红娘”
pci_device_id 表定义了驱动所支持的 PCIe 设备集合。内核在枚举 PCIe 总线时,会遍历所有已注册驱动的 id_table,寻找匹配的设备。
| 字段 | 说明 | 匹配规则 |
|---|---|---|
| vendor | 厂商 ID (VID) | 必须精确匹配,除非使用 PCI_ANY_ID |
| device | 设备 ID (DID) | 必须精确匹配,除非使用 PCI_ANY_ID |
| subvendor | 子系统厂商 ID | 可选,用于更精确匹配 |
| subdevice | 子系统设备 ID | 可选,用于更精确匹配 |
| class | 设备类别 | 可选,匹配大类设备 |
| class_mask | 类别掩码 | 与 class 配合使用 |
| driver_data | 驱动私有数据 | 匹配成功后传递给 probe |
工程经验:在链路故障排查中,
pci_device_id 表是首要检查点。如果设备无法被驱动绑定,首先确认 VID/DID 是否与硬件规格书一致。使用 lspci -n 命令可以获取实际设备的 VID:DID。
// 匹配表示例:支持多个设备
#define PCI_VENDOR_ID_MY_COMPANY 0x1234
#define PCI_DEVICE_ID_ACCEL_V1 0x5678
#define PCI_DEVICE_ID_ACCEL_V2 0x5679
static const struct pci_device_id my_pcie_ids[] = {
{ PCI_DEVICE(PCI_VENDOR_ID_MY_COMPANY, PCI_DEVICE_ID_ACCEL_V1) },
{ PCI_DEVICE(PCI_VENDOR_ID_MY_COMPANY, PCI_DEVICE_ID_ACCEL_V2) },
{ 0, } // 必须以空条目结束
};
MODULE_DEVICE_TABLE(pci, my_pcie_ids);
3.3 probe/remove 回调函数生命周期
probe 和 remove 是 PCIe 驱动生命周期中最重要的两个回调函数。它们分别在设备绑定和解除绑定时被内核调用,负责资源的初始化和清理。
3.3.1 probe 回调:驱动的“构造函数”
当 pci_device_id 匹配成功后,内核调用驱动的 probe 函数。典型的 probe 流程包括:
- 使能设备:调用
pci_enable_device()激活 PCIe 设备。 - 请求 MMIO/BAR 资源:使用
pci_request_regions()或pcim_iomap_regions()。 - 设置 DMA 掩码:调用
dma_set_mask_and_coherent()。 - 初始化硬件:读写配置空间、复位设备等。
- 注册中断:使用
request_irq()或pci_alloc_irq_vectors()。 - 创建内核接口:如注册字符设备、net_device 等。
static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
int ret;
// 1. 分配私有数据结构
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
// 2. 使能 PCIe 设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 3. 请求 BAR0 资源并 ioremap
ret = pcim_iomap_regions(pdev, BIT(0), "my_pcie");
if (ret)
goto err_disable;
dev->regs = pcim_iomap_table(pdev)[0];
// 4. 设置 DMA 掩码(64 位)
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
goto err_disable;
// 5. 设置驱动私有数据
pci_set_drvdata(pdev, dev);
// 6. 注册中断(MSI-X 优先)
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI);
if (ret < 0)
goto err_disable;
dev->irq = pci_irq_vector(pdev, 0);
ret = request_irq(dev->irq, my_pcie_isr, 0, "my_pcie", dev);
if (ret)
goto err_free_vectors;
dev_info(&pdev->dev, "Probe successful\n");
return 0;
err_free_vectors:
pci_free_irq_vectors(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
3.3.2 remove 回调:驱动的“析构函数”
remove 函数负责释放 probe 中分配的所有资源,顺序通常与 probe 相反:
- 释放中断 → 释放 MSI/MSI-X 向量 → 释放 IOMMU/DMA 映射 → 释放 MMIO 映射 → 禁用设备。
static void my_pcie_remove(struct pci_dev *pdev)
{
struct my_dev *dev = pci_get_drvdata(pdev);
// 1. 释放中断
free_irq(dev->irq, dev);
// 2. 释放 MSI/MSI-X 向量
pci_free_irq_vectors(pdev);
// 3. 禁用设备(自动释放 pcim_iomap_regions 的资源)
pci_disable_device(pdev);
// 4. 私有数据由 devm_ 自动释放
dev_info(&pdev->dev, "Remove complete\n");
}
故障排查要点:在链路故障场景中,
probe 失败是常见问题。建议在 probe 的每个关键步骤后添加 dev_dbg() 或 dev_err() 日志。使用 devm_ 系列 API 可以简化错误处理,但需注意 pci_disable_device() 必须显式调用。
3.4 工程案例:probe 失败链路排查
场景:某 FPGA 加速卡在服务器上无法被驱动绑定,
dmesg 显示 "my_pcie: probe of 0000:03:00.0 failed with error -16"。
排查步骤:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 检查 lspci -vvv -s 03:00.0 确认设备状态 |
确认设备处于正常枚举状态 |
| 2 | 查看 /sys/bus/pci/devices/0000:03:00.0/driver 链接 |
确认当前是否已被其他驱动绑定 |
| 3 | 检查 pci_enable_device 返回值 |
-16 对应 -EBUSY,设备已被占用 |
| 4 | 使用 lspci -k 查看内核驱动绑定情况 |
发现设备被 "vfio-pci" 驱动绑定 |
| 5 | 解绑冲突驱动:echo 0000:03:00.0 > /sys/bus/pci/drivers/vfio-pci/unbind |
释放设备 |
| 6 | 重新绑定目标驱动:echo 0000:03:00.0 > /sys/bus/pci/drivers/my_pcie/bind |
probe 成功 |
最佳实践:在
probe 函数中,始终使用 dev_err() 输出错误码和上下文信息。例如:dev_err(&pdev->dev, "Failed to enable device: %d\n", ret);。这能大幅缩短故障定位时间。