3. PCIe 设备枚举:pci_register_driver、pci_device_id 匹配表、probe/remove 回调
PCIe 设备枚举是驱动加载的第一个关键阶段。内核通过 pci_register_driver 注册驱动,利用 pci_device_id 匹配表与硬件 ID 进行比对,匹配成功后调用 probe 回调初始化设备,卸载时调用 remove 回调释放资源。本节将拆解这一完整流程,并给出调试条件断点的设置思路。
3.1 驱动注册与匹配流程
PCI 子系统采用 驱动-设备 分离模型。驱动注册后,内核遍历 PCI 总线上的所有设备,将驱动的 pci_device_id 表与设备的 Vendor ID、Device ID、Subsystem ID 等进行逐项比对。匹配成功则触发 probe,失败则继续等待。
调试要点: 在
pci_register_driver 返回后,立即检查 /sys/bus/pci/drivers/<驱动名>/ 目录下是否生成了设备链接。若没有,说明匹配表未命中或设备未枚举。
3.2 pci_device_id 匹配表结构
pci_device_id 是驱动与硬件之间的“身份证”对照表。内核使用 PCI_DEVICE 宏填充关键字段,匹配时按顺序扫描,直到找到完全匹配或通配匹配的条目。
| 字段 | 含义 | 调试常见问题 |
|---|---|---|
| vendor | 厂商 ID,如 0x8086 (Intel) | 大小端写反,或使用了错误的厂商 ID |
| device | 设备 ID,如 0x10fb | 与 lspci -n 输出不一致 |
| subvendor | 子系统厂商 ID,通常用 PCI_ANY_ID | 未使用通配符导致匹配失败 |
| subdevice | 子系统设备 ID,通常用 PCI_ANY_ID | 未使用通配符导致匹配失败 |
| class | 设备类别,如 0x020000 (以太网) | 类别掩码设置错误 |
| class_mask | 类别掩码,决定哪些位参与匹配 | 掩码为 0 导致全不匹配 |
| driver_data | 驱动私有数据,传递给 probe | 数据类型转换错误 |
调试技巧: 使用
lspci -n -s <BDF> 获取设备的原始 Vendor/Device ID,与驱动中的 pci_device_id 表逐字节比对。注意 PCI 配置空间中的 ID 是小端存储,但内核宏已处理字节序,驱动代码中直接写十六进制值即可。
3.3 probe 回调:设备初始化战场
probe 回调是驱动与硬件建立联系的起点。内核在匹配成功后,将 struct pci_dev * 和 struct pci_device_id * 传入 probe 函数。驱动在此完成:
- 启用设备:
pci_enable_device()开启 I/O 和内存空间。 - 设置 DMA 掩码:
dma_set_mask()和dma_set_coherent_mask()。 - 请求 MMIO/BAR 资源:
pci_request_regions()。 - 映射 BAR 空间:
pcim_iomap()或ioremap()。 - 注册中断:
request_irq()或pci_alloc_irq_vectors()。 - 创建字符/网络设备: 注册到内核框架。
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
struct my_dev *dev;
// 1. 启用 PCI 设备
ret = pci_enable_device(pdev);
if (ret)
return ret;
// 2. 设置 DMA 掩码(64 位优先)
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
if (ret)
goto err_disable;
}
// 3. 请求 MMIO 区域
ret = pci_request_regions(pdev, "my_driver");
if (ret)
goto err_disable;
// 4. 映射 BAR0
dev->regs = pcim_iomap(pdev, 0, pci_resource_len(pdev, 0));
if (!dev->regs) {
ret = -ENOMEM;
goto err_release;
}
// 5. 注册中断
ret = request_irq(pdev->irq, my_interrupt, IRQF_SHARED,
"my_driver", dev);
if (ret)
goto err_iounmap;
// 6. 保存私有数据
pci_set_drvdata(pdev, dev);
return 0;
err_iounmap:
pcim_iounmap(pdev, dev->regs);
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
常见故障: probe 返回非零值后,内核不会再次调用 probe,除非驱动重新注册或设备热插拔。如果 probe 失败,务必检查
dmesg 中的错误码,并在 probe 入口设置条件断点,观察 pdev->vendor 和 pdev->device 是否与预期一致。
3.4 remove 回调:资源清理与故障恢复
remove 回调在驱动卸载或设备热移除时被调用。其职责是 probe 的逆操作,必须按 反向顺序 释放资源,否则可能导致内核崩溃或资源泄漏。
| 步骤 | 操作 | 对应 probe 操作 |
|---|---|---|
| 1 | 禁止中断 | request_irq |
| 2 | 注销设备文件/网络设备 | 设备注册 |
| 3 | 取消 MMIO 映射 | pcim_iomap / ioremap |
| 4 | 释放 PCI 区域 | pci_request_regions |
| 5 | 禁用 PCI 设备 | pci_enable_device |
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_dev *dev = pci_get_drvdata(pdev);
// 1. 释放中断
free_irq(pdev->irq, dev);
// 2. 取消 MMIO 映射
pcim_iounmap(pdev, dev->regs);
// 3. 释放 PCI 区域
pci_release_regions(pdev);
// 4. 禁用设备
pci_disable_device(pdev);
// 5. 释放私有数据结构
kfree(dev);
}
调试条件断点设置: 在
my_pci_remove 入口设置断点,条件为 pdev->vendor == 0x8086 && pdev->device == 0x10fb。这样可以只捕获目标设备的移除事件,避免被其他设备干扰。
3.5 枚举失败调试清单
当驱动加载后 probe 未被调用,按以下清单逐项排查:
- 检查设备是否被其他驱动占用:
lspci -k -s <BDF>查看Kernel driver in use。 - 验证 pci_device_id 表: 对比
lspci -n输出,确认 vendor/device 完全一致。 - 确认驱动注册成功:
cat /sys/bus/pci/drivers/<驱动名>/是否存在。 - 检查 probe 返回值:
dmesg | grep <驱动名>搜索错误码。 - 使用条件断点跟踪匹配循环: 在
pci_bus_match或pci_match_device设置断点,观察匹配过程。
3.6 条件断点实战:跟踪 probe 调用
在调试 probe 未被调用的场景时,条件断点是最有效的工具。以下是在 kgdb 或 gdb 中设置条件断点的示例:
# 在 probe 函数入口设置条件断点,只捕获目标设备
(gdb) break my_pci_probe if pdev->vendor == 0x8086 && pdev->device == 0x10fb
# 在 pci_match_device 中观察匹配过程
(gdb) break pci_match_device if id->vendor == 0x8086
# 在 pci_bus_match 中观察总线匹配
(gdb) break pci_bus_match if dev->vendor == 0x8086
内核调试配置: 确保内核开启了
CONFIG_DEBUG_INFO 和 CONFIG_GDB_SCRIPTS,否则无法使用条件断点中的结构体字段。使用 lx-symbols 加载驱动符号表,确保断点地址正确。