4. PCIe驱动模型:Linux内核PCI子系统架构、pci_driver结构体、probe/remove回调机制

4.1 Linux内核PCI子系统整体架构

Linux内核PCI子系统采用分层设计,从顶层设备驱动到底层硬件访问,共分为三个核心层次:PCI核心层、PCI总线驱动层和PCI设备驱动层。理解这一分层结构是编写正确PCIe驱动的先决条件。

关键概念: PCI子系统通过pci_bus、pci_dev、pci_driver三个核心数据结构管理所有PCI/PCIe设备。驱动开发者主要与pci_driver交互,而枚举、配置空间访问、DMA映射等底层操作由核心层封装。
Linux PCI子系统分层架构 PCI设备驱动层 pci_driver | probe() | remove() | suspend() | resume() PCI核心层 (PCI Core) pci_register_driver() | 设备枚举 | 配置空间访问 | 资源分配 PCI总线驱动层 (Host Bridge / Root Complex) 配置空间访问原语 | MMIO/PIO | 中断路由 | DMA地址转换 驱动开发者只需实现pci_driver回调,核心层自动完成设备匹配与资源准备

4.2 pci_driver结构体深度解析

pci_driver是PCI设备驱动的核心载体,定义在include/linux/pci.h中。驱动通过填充该结构体并向内核注册,实现与PCI子系统的绑定。

成员名称类型功能描述
nameconst char *驱动名称,用于sysfs标识和错误日志
id_tableconst struct pci_device_id *设备ID匹配表,定义驱动支持的Vendor/Device/Class
probeint (*probe)(struct pci_dev *, const struct pci_device_id *)设备匹配成功时调用,完成硬件初始化和数据结构创建
removevoid (*remove)(struct pci_dev *)设备移除或驱动卸载时调用,释放资源
suspendint (*suspend)(struct pci_dev *, pm_message_t)电源管理:挂起设备
resumeint (*resume)(struct pci_dev *)电源管理:恢复设备
shutdownvoid (*shutdown)(struct pci_dev *)系统关机时调用
driverstruct device_driver内嵌通用设备驱动结构体
开发提示: id_table是驱动与硬件匹配的关键。使用PCI_DEVICE(vendor, device)宏精确匹配单设备,或使用PCI_DEVICE_CLASS(class, class_mask)匹配设备类别。匹配表必须以空条目{0, }结尾。

4.3 probe/remove回调机制与生命周期

probe()和remove()是PCI驱动中最重要的两个回调函数,它们定义了设备从发现到移除的完整生命周期。

4.3.1 probe()回调执行流程

当PCI核心层检测到设备且id_table匹配成功后,按以下顺序调用probe():

  1. 使能设备:调用pci_enable_device(),激活I/O和内存空间
  2. 请求资源:调用pci_request_regions(),声明BAR空间所有权
  3. 设置DMA掩码:调用dma_set_mask()和dma_set_coherent_mask()
  4. 读取配置信息:通过pci_read_config_*()获取设备能力寄存器
  5. 映射BAR空间:使用pci_iomap()或ioremap()将物理地址映射到内核虚拟地址
  6. 注册中断:调用request_irq()或pci_request_irq()注册中断处理函数
  7. 初始化私有数据结构:分配并填充驱动自定义的私有数据
  8. 注册子设备/接口:如misc设备、net_device、cdev等
probe() 回调执行流程 id_table 匹配成功 pci_enable_device() pci_request_regions() dma_set_mask() 设置 pci_iomap() / ioremap() request_irq() 注册中断 probe() 返回 0 (成功)

4.3.2 remove()回调执行流程

remove()是probe()的逆操作,在设备热拔插、驱动卸载或系统关机时被调用:

  • 停止所有I/O操作:确保没有正在进行的DMA或中断处理
  • 注销子设备:移除通过probe注册的misc/net/char设备
  • 释放中断:调用free_irq()或pci_free_irq()
  • 取消映射BAR:调用pci_iounmap()释放虚拟地址映射
  • 释放资源:调用pci_release_regions()
  • 禁用设备:调用pci_disable_device()
  • 释放私有数据:kfree驱动分配的私有结构体
常见错误: remove()中遗漏pci_release_regions()会导致下次加载驱动时资源被占用,pci_request_regions()返回-EBUSY。务必保证probe和remove的资源操作完全对称。

4.4 完整驱动模板与代码示例

以下是一个标准的PCIe驱动框架,展示了pci_driver结构体的完整实现:

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/fs.h>

#define DRIVER_NAME "pcie_sample"
#define VENDOR_ID   0x10EE  /* 示例厂商ID */
#define DEVICE_ID   0x7011  /* 示例设备ID */

/* 设备私有数据结构 */
struct pcie_dev_priv {
    struct pci_dev *pdev;
    void __iomem   *bar0_addr;
    unsigned long   bar0_phys;
    int             irq_num;
};

/* ID匹配表 */
static const struct pci_device_id pcie_id_table[] = {
    { PCI_DEVICE(VENDOR_ID, DEVICE_ID) },
    { 0, }
};
MODULE_DEVICE_TABLE(pci, pcie_id_table);

/* probe回调 */
static int pcie_probe(struct pci_dev *pdev,
                      const struct pci_device_id *id)
{
    struct pcie_dev_priv *priv;
    int ret;

    /* 1. 分配私有数据 */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 2. 使能PCI设备 */
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    /* 3. 请求BAR资源 */
    ret = pci_request_regions(pdev, DRIVER_NAME);
    if (ret)
        goto err_disable;

    /* 4. 设置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_release;
    }

    /* 5. 映射BAR0 */
    priv->bar0_addr = pci_iomap(pdev, 0, 0);
    if (!priv->bar0_addr) {
        ret = -EIO;
        goto err_release;
    }
    priv->bar0_phys = pci_resource_start(pdev, 0);

    /* 6. 注册中断(MSI优先) */
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        goto err_unmap;

    priv->irq_num = pci_irq_vector(pdev, 0);
    priv->pdev = pdev;
    pci_set_drvdata(pdev, priv);

    dev_info(&pdev->dev, "PCIe device probed: BAR0=0x%lx\n",
             priv->bar0_phys);
    return 0;

err_unmap:
    pci_iounmap(pdev, priv->bar0_addr);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

/* remove回调 */
static void pcie_remove(struct pci_dev *pdev)
{
    struct pcie_dev_priv *priv = pci_get_drvdata(pdev);

    /* 逆序释放probe中获取的资源 */
    pci_free_irq_vectors(pdev);
    pci_iounmap(pdev, priv->bar0_addr);
    pci_release_regions(pdev);
    pci_disable_device(pdev);

    dev_info(&pdev->dev, "PCIe device removed\n");
}

/* pci_driver结构体定义 */
static struct pci_driver pcie_driver = {
    .name     = DRIVER_NAME,
    .id_table = pcie_id_table,
    .probe    = pcie_probe,
    .remove   = pcie_remove,
};

/* 模块入口/出口 */
module_pci_driver(pcie_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("PCIe Driver Expert");
MODULE_DESCRIPTION("PCIe Sample Driver for JTAG Debug");

4.5 设备匹配与加载时序

PCIe驱动加载和设备匹配遵循以下时序:

步骤执行者动作描述
1内核启动/模块加载执行module_pci_driver(),调用pci_register_driver()
2PCI核心层遍历已枚举的pci_dev链表,与id_table逐条匹配
3PCI核心层匹配成功则调用驱动probe(),传入pci_dev和匹配的pci_device_id
4驱动probe()执行硬件初始化,返回0表示成功,负值表示失败
5PCI核心层probe成功后将驱动与设备绑定,设备进入操作状态
6驱动卸载调用pci_unregister_driver(),触发所有绑定设备的remove()
调试要点: 使用lspci -vvv查看设备当前驱动绑定状态;/sys/bus/pci/devices/目录下每个设备都有driver符号链接指向当前绑定的驱动。若probe失败,检查dmesg中的错误码和pci_enable_device返回值。

4.6 常见probe失败原因与排查

  • -ENODEV:设备未使能或配置空间访问失败,检查pci_enable_device()是否被调用
  • -EBUSY:BAR资源已被其他驱动占用,使用lspci -vv查看Region状态
  • -EIO:BAR映射失败,检查BAR地址是否有效,或尝试使用ioremap()替代pci_iomap()
  • -ENOMEM:内存分配失败,检查GFP标志或减少分配大小
  • -EINVAL:中断注册参数错误,确认irq_handler_t函数签名正确
PCIe驱动加载与设备匹配时序 insmod / modprobe pci_register_driver() id_table 匹配 probe() 执行 匹配与加载关键点 • 驱动注册后立即扫描所有已枚举设备 • 匹配基于 vendor_id + device_id + class • probe失败不会影响其他设备匹配 • 热拔插设备会动态触发probe/remove • 使用 pci_get_drvdata() 在回调间传递数据 • 驱动卸载时自动调用所有设备的remove() • 可通过 /sys/bus/pci/drivers/ 手动绑定/解绑 • 调试时使用 echo "bind" / "unbind" 测试

4.7 本章小结

PCIe驱动模型的核心是pci_driver结构体及其probe/remove回调机制。驱动开发者需要掌握:

  • PCI子系统三层架构:设备驱动层 → PCI核心层 → 总线驱动层
  • pci_driver各成员的正确填充方式,特别是id_table的匹配规则
  • probe()中严格的资源获取顺序和错误处理路径
  • remove()中与probe完全对称的资源释放顺序
  • 通过module_pci_driver()宏简化模块注册
下一阶段准备: 在JTAG调试链路中,probe()的成功执行是后续通过JTAG访问PCIe配置空间和BAR内存的前提。确保probe()中正确映射BAR空间,是JTAG调试链路分析的基础。