3. PCIe设备枚举与发现:Linux PCI子系统初始化、pci_device_id匹配机制、probe()与remove()回调函数实现

PCIe设备的枚举与发现是驱动开发的基础环节。本章深入剖析Linux PCI子系统的初始化流程、设备ID匹配机制以及核心回调函数的实现模式,帮助开发者建立从硬件拓扑到驱动加载的完整认知链路。

3.1 Linux PCI子系统初始化流程

系统启动时,PCI子系统经历四个关键阶段完成初始化:

  1. ACPI/DT解析阶段:固件通过ACPI表或设备树描述PCIe主桥(Root Complex)的地址空间与中断映射。
  2. PCI总线扫描阶段:内核调用pci_scan_bus()递归遍历总线拓扑,为每个设备分配BDF(Bus:Device.Function)编号。
  3. 资源分配阶段:pci_assign_unassigned_resources()为设备分配MMIO、I/O端口及总线号。
  4. 设备注册阶段:每个PCI设备被封装为struct pci_dev,挂入内核设备模型。
关键数据结构:struct pci_bus 代表一条PCI总线,struct pci_dev 代表一个功能设备,两者通过bus_list和dev_list双向链表组织。
PCI子系统初始化流程 ACPI/DT解析 pci_acpi_scan_root() 总线扫描 pci_scan_bus() 资源分配 pci_assign_unassigned() 设备注册 pci_bus_add_devices() 💡 初始化顺序依赖 • 必须先完成ACPI/DT解析,才能获得主桥的配置空间基址。 • 资源分配必须在设备注册之前,否则设备无法获取有效地址。 • 设备注册触发驱动匹配,即进入 pci_device_id 匹配环节。

3.2 pci_device_id 匹配机制

当PCI设备注册到内核后,驱动核心根据pci_device_id表进行匹配。该表定义了驱动支持的设备集合,匹配字段包括:

字段 说明 匹配规则
vendor 厂商ID(如0x8086为Intel) 精确匹配
device 设备ID(如0x10fb为82599ES) 精确匹配
subvendor 子系统厂商ID 可设为PCI_ANY_ID通配
subdevice 子系统设备ID 可设为PCI_ANY_ID通配
class 设备类别(如0x020000为以太网) 支持掩码匹配
class_mask 类别掩码 与class按位与后比较
driver_data 驱动私有数据指针 匹配成功后传递给probe()
最佳实践:优先使用vendor+device精确匹配,避免仅依赖class匹配导致驱动被错误加载到不兼容设备上。

匹配流程在内核函数pci_match_device()中实现,其核心逻辑如下:

static const struct pci_device_id *pci_match_device(
    struct pci_driver *drv, struct pci_dev *dev)
{
    const struct pci_device_id *id;

    /* 遍历驱动支持的ID表 */
    for (id = drv->id_table; id->vendor || id->subvendor ||
         id->class_mask; id++) {
        /* 检查vendor匹配 */
        if ((id->vendor != PCI_ANY_ID) &&
            (id->vendor != dev->vendor))
            continue;

        /* 检查device匹配 */
        if ((id->device != PCI_ANY_ID) &&
            (id->device != dev->device))
            continue;

        /* 检查subvendor/subdevice */
        if ((id->subvendor != PCI_ANY_ID) &&
            (id->subvendor != dev->subsystem_vendor))
            continue;

        if ((id->subdevice != PCI_ANY_ID) &&
            (id->subdevice != dev->subsystem_device))
            continue;

        /* 检查class掩码匹配 */
        if ((id->class_mask & id->class) !=
            (id->class_mask & dev->class))
            continue;

        return id;  /* 匹配成功 */
    }
    return NULL;    /* 无匹配 */
}
pci_device_id 匹配决策树 设备注册 vendor + device 匹配? 跳过该驱动 subvendor/subdevice 匹配? class + class_mask 匹配? 调用 probe()

3.3 probe() 回调函数实现

当pci_device_id匹配成功后,内核调用驱动的probe()函数。该函数是驱动初始化的核心入口,典型实现包含以下步骤:

  1. 启用设备:调用pci_enable_device()使能PCI配置空间中的I/O和内存空间位。
  2. 请求资源:使用pci_request_regions()声明设备的BAR空间所有权。
  3. 设置DMA掩码:通过dma_set_mask()和dma_set_coherent_mask()配置设备DMA能力。
  4. 映射BAR空间:使用pci_iomap()将BAR物理地址映射为内核虚拟地址。
  5. 初始化硬件:配置设备寄存器、中断、队列等硬件资源。
  6. 注册设备接口:如网络设备调用register_netdev(),字符设备调用cdev_add()。
常见错误:probe()中未检查pci_enable_device()返回值,导致设备未正确使能时继续执行后续操作,引发系统崩溃或内存访问异常。

以下是一个完整的probe()实现模板:

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

    /* 1. 分配私有数据结构 */
    adapter = kzalloc(sizeof(*adapter), GFP_KERNEL);
    if (!adapter)
        return -ENOMEM;

    pci_set_drvdata(pdev, adapter);

    /* 2. 启用PCI设备 */
    ret = pci_enable_device(pdev);
    if (ret) {
        dev_err(&pdev->dev, "pci_enable_device failed: %d\n", ret);
        goto err_free;
    }

    /* 3. 请求BAR资源 */
    ret = pci_request_regions(pdev, DRV_NAME);
    if (ret) {
        dev_err(&pdev->dev, "pci_request_regions failed: %d\n", ret);
        goto err_disable;
    }

    /* 4. 设置DMA掩码(64位优先,回退到32位) */
    ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
    if (ret) {
        ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(32));
        if (ret) {
            dev_err(&pdev->dev, "dma_set_mask failed: %d\n", ret);
            goto err_regions;
        }
    }

    /* 5. 映射BAR0 */
    adapter->bar0 = pci_iomap(pdev, 0, 0);
    if (!adapter->bar0) {
        dev_err(&pdev->dev, "pci_iomap BAR0 failed\n");
        ret = -ENOMEM;
        goto err_regions;
    }

    /* 6. 读取设备配置信息 */
    pci_read_config_word(pdev, PCI_STATUS, &adapter->status);

    /* 7. 设置总线主控(DMA使能) */
    pci_set_master(pdev);

    /* 8. 注册网络设备(示例) */
    adapter->netdev = alloc_etherdev(sizeof(struct my_priv));
    if (!adapter->netdev) {
        ret = -ENOMEM;
        goto err_iounmap;
    }

    ret = register_netdev(adapter->netdev);
    if (ret) {
        dev_err(&pdev->dev, "register_netdev failed: %d\n", ret);
        goto err_free_netdev;
    }

    dev_info(&pdev->dev, "device %04x:%04x probed successfully\n",
             pdev->vendor, pdev->device);
    return 0;

err_free_netdev:
    free_netdev(adapter->netdev);
err_iounmap:
    pci_iounmap(pdev, adapter->bar0);
err_regions:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
err_free:
    kfree(adapter);
    return ret;
}

3.4 remove() 回调函数实现

remove()函数是probe()的逆操作,负责释放所有已分配的资源。其执行顺序必须与probe()严格相反:

步骤 操作 对应probe()步骤
1 注销设备接口(如unregister_netdev) 8
2 释放网络设备(free_netdev) 8
3 取消BAR映射(pci_iounmap) 5
4 释放BAR资源(pci_release_regions) 3
5 禁用PCI设备(pci_disable_device) 2
6 释放私有数据(kfree) 1
static void my_pci_remove(struct pci_dev *pdev)
{
    struct my_adapter *adapter = pci_get_drvdata(pdev);

    if (!adapter)
        return;

    /* 1. 注销网络设备 */
    if (adapter->netdev) {
        unregister_netdev(adapter->netdev);
        free_netdev(adapter->netdev);
    }

    /* 2. 取消BAR映射 */
    if (adapter->bar0)
        pci_iounmap(pdev, adapter->bar0);

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

    /* 4. 释放私有数据 */
    kfree(adapter);
    pci_set_drvdata(pdev, NULL);

    dev_info(&pdev->dev, "device removed\n");
}
关键原则:remove()必须能够处理probe()执行到一半失败的情况。例如,如果probe()在注册网络设备之前失败,remove()应检查adapter->netdev是否为NULL再决定是否调用unregister_netdev()。
probe() 与 remove() 资源生命周期 probe() 分配顺序 ① kzalloc 私有数据 ② pci_enable_device ③ pci_request_regions ④ pci_iomap + register_netdev remove() 释放顺序 ① unregister_netdev ② pci_iounmap ③ pci_release_regions ④ pci_disable_device + kfree

3.5 实践要点与常见陷阱

  • ID表必须终止:pci_device_id数组必须以全零条目结束,否则内核遍历时会越界。
  • probe()失败回滚:使用goto标签实现分层错误处理,确保已分配的资源全部释放。
  • 并发安全:probe()和remove()可能在不同CPU上并发执行,需使用锁保护共享数据结构。
  • 热插拔支持:现代PCIe支持热插拔,remove()必须能够随时被调用,不能假设设备始终存在。
  • 调试手段:在probe()入口和remove()出口添加dev_dbg()日志,配合pci_info()输出设备BDF信息。
调试技巧:使用echo 1 > /sys/bus/pci/devices/0000:XX:XX.X/remove手动触发remove(),验证资源释放是否完整。再通过echo 1 > /sys/bus/pci/rescan重新枚举设备,测试probe()的健壮性。