Linux内核PCIe驱动调试 反汇编分析与链路故障定位 - 第2节
2. Linux内核PCIe子系统架构
Linux 内核 PCIe 子系统使用两个核心数据结构来抽象物理拓扑:pci_bus 表示一条 PCI 总线,pci_dev 表示总线上的一个设备(包括桥设备)。理解这两个结构体是调试链路故障的基础。
2.1.1 pci_bus 结构体关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
number | u8 | 总线号(Primary Bus Number) |
primary | u8 | 上游桥接的总线号 |
secondary | u8 | 下游桥接的总线号 |
subordinate | u8 | 子树最大总线号 |
bridge | struct device * | 指向桥设备的通用 device 结构 |
devices | struct list_head | 挂载在此总线上的设备链表头 |
children | struct list_head | 子总线链表头 |
ops | struct pci_ops * | 总线访问操作函数集(配置空间读写) |
调试要点: 当链路训练失败时,
secondary 和 subordinate 可能为 0,表明桥设备未能成功枚举下游总线。通过 /sys/devices/pci*/pci_bus/*/ 可查看总线号分配。
2.1.2 pci_dev 结构体关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
dev | struct device | 通用设备模型基类 |
bus | struct pci_bus * | 所属总线 |
vendor | u16 | 厂商 ID(Vendor ID) |
device | u16 | 设备 ID(Device ID) |
subsystem_vendor | u16 | 子系统厂商 ID |
subsystem_device | u16 | 子系统设备 ID |
class | u32 | 类别寄存器(Base Class / Sub Class) |
irq | unsigned int | 中断号 |
current_state | pci_power_t | 当前电源状态(D0-D3hot) |
driver | struct pci_driver * | 绑定的驱动指针 |
反汇编提示: 在 crash dump 或 kgdb 中,通过
pci_dev 的 vendor 和 device 字段可快速定位异常设备。若 driver 为 NULL,说明设备未被任何驱动绑定,可能是 ID 匹配失败或 probe 未执行。
2.1.3 数据结构关系图
2.2 pci_driver 注册流程
PCIe 设备驱动通过 pci_register_driver() 向内核注册,内核在枚举设备时自动匹配并调用 probe。整个流程涉及设备模型、PCI 核心和电源管理框架。
2.2.1 注册流程步骤
- 驱动初始化:驱动模块入口调用
pci_register_driver(),传入struct pci_driver实例。 - ID 表注册:内核将
pci_driver->id_table加入 PCI 子系统 ID 匹配链表。 - 设备枚举触发:对系统中所有已枚举的
pci_dev,遍历 ID 表进行匹配。 - 匹配成功:调用
pci_driver->probe(),传入匹配的pci_dev指针。 - 资源分配:probe 函数中调用
pci_enable_device()、pci_request_regions()等 API。 - 中断注册:通过
request_irq()或pci_alloc_irq_vectors()注册中断处理。 - 设备添加:调用
pci_set_drvdata()保存私有数据,设备进入就绪状态。
2.2.2 pci_driver 结构体关键成员
| 成员 | 类型 | 说明 |
|---|---|---|
name | const char * | 驱动名称,用于 sysfs 和调试 |
id_table | struct pci_device_id * | 支持的设备 ID 列表(vendor/device 匹配) |
probe | int (*)(struct pci_dev *, const struct pci_device_id *) | 设备匹配成功时调用 |
remove | void (*)(struct pci_dev *) | 设备移除或驱动卸载时调用 |
suspend | int (*)(struct pci_dev *, pm_message_t) | 电源管理挂起回调 |
resume | int (*)(struct pci_dev *) | 电源管理恢复回调 |
shutdown | void (*)(struct pci_dev *) | 系统关机时调用 |
调试要点: 若 probe 未被执行,检查
id_table 中的 vendor/device 是否与硬件配置空间一致。使用 lspci -nn 获取实际 ID,对比驱动源码中的 PCI_VDEVICE 宏定义。
2.2.3 注册与匹配流程图
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 调用时序图
2.3.4 probe 失败常见原因
| 错误码 | 常见原因 | 调试方法 |
|---|---|---|
-ENODEV | 设备未就绪或电源状态异常 | 检查 current_state 是否为 D0;检查链路训练状态 |
-EBUSY | 资源已被占用(BAR 冲突) | 使用 lspci -vv 查看 BAR 地址范围是否重叠 |
-EIO | BAR 映射失败或配置空间访问超时 | 检查 pci_iomap 返回值;确认 MMIO 地址有效 |
-ENOMEM | 内存分配失败 | 检查系统内存压力;使用 devm_kzalloc 替代 kzalloc |
-EINVAL | DMA 掩码设置不匹配 | 确认设备支持的 DMA 位宽;检查 dma_set_mask 参数 |
反汇编分析技巧: 当 probe 返回负错误码时,在
pci_device_probe() 函数入口设置断点,单步跟踪到错误路径。重点关注 pci_enable_device 内部对 pci_read_config_word 的调用,配置空间读取失败往往是链路问题的第一信号。