3. PCIe设备枚举

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/<驱动名>/ 目录下是否生成了设备链接。若没有,说明匹配表未命中或设备未枚举。
PCIe 驱动注册与匹配流程 pci_register_driver pci_device_id 表 遍历 PCI 设备列表 Vendor/Device ID 比对 匹配成功 probe 回调 设备初始化 remove 回调 调试条件断点位置 pci_device_id 比对循环 probe 入口函数 remove 入口函数

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 未被调用,按以下清单逐项排查:

  1. 检查设备是否被其他驱动占用: lspci -k -s <BDF> 查看 Kernel driver in use。
  2. 验证 pci_device_id 表: 对比 lspci -n 输出,确认 vendor/device 完全一致。
  3. 确认驱动注册成功: cat /sys/bus/pci/drivers/<驱动名>/ 是否存在。
  4. 检查 probe 返回值: dmesg | grep <驱动名> 搜索错误码。
  5. 使用条件断点跟踪匹配循环: 在 pci_bus_match 或 pci_match_device 设置断点,观察匹配过程。
PCIe 枚举失败调试决策树 probe 未被调用 检查驱动是否注册成功 /sys/bus/pci/drivers/ 存在? 检查设备是否被占用 lspci -k 显示其他驱动? 检查 pci_device_id 表 Vendor/Device 匹配? 设置条件断点:pci_bus_match + pci_match_device 观察匹配循环中的 id_table 遍历过程

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 加载驱动符号表,确保断点地址正确。