Linux内核PCIe驱动调试 反汇编分析与链路故障定位 - 第2节

2. Linux内核PCIe子系统架构

Linux 内核 PCIe 子系统使用两个核心数据结构来抽象物理拓扑:pci_bus 表示一条 PCI 总线,pci_dev 表示总线上的一个设备(包括桥设备)。理解这两个结构体是调试链路故障的基础。

2.1.1 pci_bus 结构体关键字段

字段类型说明
numberu8总线号(Primary Bus Number)
primaryu8上游桥接的总线号
secondaryu8下游桥接的总线号
subordinateu8子树最大总线号
bridgestruct device *指向桥设备的通用 device 结构
devicesstruct list_head挂载在此总线上的设备链表头
childrenstruct list_head子总线链表头
opsstruct pci_ops *总线访问操作函数集(配置空间读写)
调试要点: 当链路训练失败时,secondary 和 subordinate 可能为 0,表明桥设备未能成功枚举下游总线。通过 /sys/devices/pci*/pci_bus/*/ 可查看总线号分配。

2.1.2 pci_dev 结构体关键字段

字段类型说明
devstruct device通用设备模型基类
busstruct pci_bus *所属总线
vendoru16厂商 ID(Vendor ID)
deviceu16设备 ID(Device ID)
subsystem_vendoru16子系统厂商 ID
subsystem_deviceu16子系统设备 ID
classu32类别寄存器(Base Class / Sub Class)
irqunsigned int中断号
current_statepci_power_t当前电源状态(D0-D3hot)
driverstruct pci_driver *绑定的驱动指针
反汇编提示: 在 crash dump 或 kgdb 中,通过 pci_dev 的 vendor 和 device 字段可快速定位异常设备。若 driver 为 NULL,说明设备未被任何驱动绑定,可能是 ID 匹配失败或 probe 未执行。

2.1.3 数据结构关系图

PCIe 子系统核心数据结构关系 struct pci_bus number: u8 secondary: u8 subordinate: u8 struct pci_dev vendor: 0x10EE device: 0x7011 driver: pci_driver * devices 链表 子 pci_bus children 链表 pci_dev (桥) bridge 指针 说明:pci_bus 通过 devices 链表管理挂载的设备,通过 children 链表管理子总线。 桥设备同时作为 pci_dev 和 pci_bus 的 bridge 指针,连接上下两层拓扑。

2.2 pci_driver 注册流程

PCIe 设备驱动通过 pci_register_driver() 向内核注册,内核在枚举设备时自动匹配并调用 probe。整个流程涉及设备模型、PCI 核心和电源管理框架。

2.2.1 注册流程步骤

  1. 驱动初始化:驱动模块入口调用 pci_register_driver(),传入 struct pci_driver 实例。
  2. ID 表注册:内核将 pci_driver->id_table 加入 PCI 子系统 ID 匹配链表。
  3. 设备枚举触发:对系统中所有已枚举的 pci_dev,遍历 ID 表进行匹配。
  4. 匹配成功:调用 pci_driver->probe(),传入匹配的 pci_dev 指针。
  5. 资源分配:probe 函数中调用 pci_enable_device()、pci_request_regions() 等 API。
  6. 中断注册:通过 request_irq() 或 pci_alloc_irq_vectors() 注册中断处理。
  7. 设备添加:调用 pci_set_drvdata() 保存私有数据,设备进入就绪状态。

2.2.2 pci_driver 结构体关键成员

成员类型说明
nameconst char *驱动名称,用于 sysfs 和调试
id_tablestruct pci_device_id *支持的设备 ID 列表(vendor/device 匹配)
probeint (*)(struct pci_dev *, const struct pci_device_id *)设备匹配成功时调用
removevoid (*)(struct pci_dev *)设备移除或驱动卸载时调用
suspendint (*)(struct pci_dev *, pm_message_t)电源管理挂起回调
resumeint (*)(struct pci_dev *)电源管理恢复回调
shutdownvoid (*)(struct pci_dev *)系统关机时调用
调试要点: 若 probe 未被执行,检查 id_table 中的 vendor/device 是否与硬件配置空间一致。使用 lspci -nn 获取实际 ID,对比驱动源码中的 PCI_VDEVICE 宏定义。

2.2.3 注册与匹配流程图

pci_driver 注册与设备匹配流程 pci_register_driver() ID 表注册 遍历 pci_dev 链表 ID 匹配? 否 跳过 是 调用 probe()

2.3 probe/remove 回调机制

probe 和 remove 是 PCIe 驱动生命周期中最重要的两个回调,分别在设备绑定和解除绑定时执行。理解其调用时机和典型实现,对调试驱动加载失败、资源泄漏等问题至关重要。

2.3.1 probe 回调典型实现

static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_dev *priv;
    int ret;

    // 1. 启用 PCI 设备(包括总线主控、IO/内存空间)
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    // 2. 请求 BAR 资源
    ret = pci_request_regions(pdev, "my_driver");
    if (ret)
        goto err_disable;

    // 3. 设置 DMA 掩码
    ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
    if (ret)
        goto err_release;

    // 4. 分配私有数据结构
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv) {
        ret = -ENOMEM;
        goto err_release;
    }

    // 5. 映射 BAR 地址
    priv->bar_addr = pci_iomap(pdev, 0, 0);
    if (!priv->bar_addr) {
        ret = -EIO;
        goto err_free;
    }

    // 6. 注册中断
    ret = request_irq(pdev->irq, my_isr, IRQF_SHARED, "my_driver", priv);
    if (ret)
        goto err_unmap;

    // 7. 保存私有数据
    pci_set_drvdata(pdev, priv);

    // 8. 启用设备(设置 Bus Master 等)
    pci_set_master(pdev);

    return 0;

err_unmap:
    pci_iounmap(pdev, priv->bar_addr);
err_free:
    devm_kfree(&pdev->dev, priv);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
常见故障: probe 失败时未正确释放已获取的资源,导致后续重试或 remove 时出现 double-free。建议优先使用 devm_* 系列 API(如 devm_kzalloc、devm_request_irq),由设备模型自动管理资源释放。

2.3.2 remove 回调典型实现

static void my_pci_remove(struct pci_dev *pdev)
{
    struct my_dev *priv = pci_get_drvdata(pdev);

    if (!priv)
        return;

    // 1. 释放中断
    free_irq(pdev->irq, priv);

    // 2. 取消映射 BAR
    pci_iounmap(pdev, priv->bar_addr);

    // 3. 释放资源
    pci_release_regions(pdev);

    // 4. 禁用设备
    pci_disable_device(pdev);

    // 5. 私有数据由 devm 自动释放
}

2.3.3 probe/remove 调用时序图

probe / remove 回调调用时序 时间 PCI 核心 驱动 probe pci_device_probe() pci_enable_device pci_request_regions request_irq 返回 0 (成功) PCI 核心 驱动 remove pci_device_remove()

2.3.4 probe 失败常见原因

错误码常见原因调试方法
-ENODEV设备未就绪或电源状态异常检查 current_state 是否为 D0;检查链路训练状态
-EBUSY资源已被占用(BAR 冲突)使用 lspci -vv 查看 BAR 地址范围是否重叠
-EIOBAR 映射失败或配置空间访问超时检查 pci_iomap 返回值;确认 MMIO 地址有效
-ENOMEM内存分配失败检查系统内存压力;使用 devm_kzalloc 替代 kzalloc
-EINVALDMA 掩码设置不匹配确认设备支持的 DMA 位宽;检查 dma_set_mask 参数
反汇编分析技巧: 当 probe 返回负错误码时,在 pci_device_probe() 函数入口设置断点,单步跟踪到错误路径。重点关注 pci_enable_device 内部对 pci_read_config_word 的调用,配置空间读取失败往往是链路问题的第一信号。