网络设备驱动开发:从PCI探测到NAPI轮询 - 第4节

4.1 PCI 设备枚举:内核如何发现硬件

当系统上电或插入新的 PCIe 设备时,内核首先通过 PCI 配置空间完成硬件拓扑的扫描。这一过程完全由 PCI 子系统 独立完成,与具体的设备驱动无关。枚举的核心目标是:为每个 PCI 功能分配唯一的 struct pci_dev 结构体,并建立从总线号→设备号→功能号的层次树。

4.1.1 配置空间与 BDF 寻址

每个 PCI 功能拥有 256 字节(PCIe 扩展为 4096 字节)的配置空间。内核通过 BDF(Bus:Device.Function)三元组定位设备:

字段位宽说明
Bus Number8 bit0~255,最多 256 条总线
Device Number5 bit0~31,每条总线最多 32 个设备
Function Number3 bit0~7,每个设备最多 8 个功能

枚举时,内核从总线 0 开始,递归扫描每个设备的功能 0,读取 Vendor ID(偏移 0x00)和 Device ID(偏移 0x02)。若 Vendor ID 为 0xFFFF,表示该槽位无设备,跳过。

关键数据结构:struct pci_dev 包含 devfn(编码设备+功能号)、bus(所属总线指针)、vendor、device、class 等字段。每个 struct pci_bus 维护 devices 链表,挂接该总线下的所有设备。

4.1.2 枚举流程:从 Root Bridge 到叶子节点

  1. 扫描 Root Bridge:ACPI 或 Device Tree 告知系统主桥的配置空间基址。
  2. 遍历总线 0:读取每个 Device Function 0 的 Vendor ID。
  3. 检测 PCI-PCI Bridge:若设备 Class Code 为 0x060400,则读取其 Secondary Bus Number 寄存器,递归扫描下级总线。
  4. 分配资源:为每个设备分配 MMIO、I/O 端口、IRQ 等资源,写入配置空间的 BAR 寄存器。
  5. 创建 pci_dev:将设备加入全局链表 pci_devices,并触发 pci_bus_add_device。
PCI 枚举流程 扫描 Root Bridge 遍历总线 0 设备 读取 Vendor/Device ID 是 PCI-PCI Bridge? 普通设备:分配资源 创建 pci_dev 加入链表 递归扫描下级总线 资源分配完成
避坑提示:PCI 枚举阶段不加载任何驱动,只建立设备拓扑。若 BAR 资源分配失败(如 64-bit 地址对齐问题),后续 probe 将无法访问设备 MMIO 空间。

4.2 pci_register_driver:驱动注册的入口

驱动开发者通过 pci_register_driver 将 struct pci_driver 注册到内核 PCI 核心。该函数是驱动与 PCI 子系统交互的起点,内部完成以下关键动作:

  • 将驱动加入全局链表 pci_drivers。
  • 遍历已枚举的所有 pci_dev,调用 匹配逻辑 检查驱动是否支持该设备。
  • 若匹配成功,调用驱动的 probe 回调。

4.2.1 struct pci_driver 核心字段

字段类型说明
nameconst char *驱动名称,用于 sysfs 和调试
id_tablestruct pci_device_id *设备 ID 表,定义支持的 Vendor/Device/Subsystem/Class
probeint (*)(struct pci_dev *, const struct pci_device_id *)设备匹配成功后的初始化回调
removevoid (*)(struct pci_dev *)设备移除或驱动卸载时的清理回调
suspend / resume电源管理回调系统休眠/唤醒时的设备状态保存与恢复
// 网络设备驱动典型注册示例
static struct pci_device_id my_net_pci_ids[] = {
    { PCI_DEVICE(0x10EC, 0x8168) },  // Realtek RTL8168
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_net_pci_ids);

static struct pci_driver my_net_driver = {
    .name     = "my_net_driver",
    .id_table = my_net_pci_ids,
    .probe    = my_net_probe,
    .remove   = my_net_remove,
};

module_pci_driver(my_net_driver);  // 展开为 module_init/exit + pci_register_driver
注意:module_pci_driver 宏自动处理 pci_register_driver 和 pci_unregister_driver 的配对。若手动实现 module_init,务必在错误路径调用 pci_unregister_driver,否则内核会泄漏驱动引用。

4.3 匹配机制:从 ID Table 到 probe 调用

PCI 核心的匹配函数 pci_match_device 是驱动与设备之间的“红娘”。它按照以下优先级顺序检查:

  1. Vendor ID + Device ID:最精确匹配,使用 PCI_DEVICE(vendor, device) 宏。
  2. Subsystem Vendor + Subsystem Device:用于区分同一芯片的不同板卡版本。
  3. Class Code:匹配设备类别(如网络控制器 Class=0x020000),用于通用驱动。
  4. 通配符:PCI_ANY_ID 匹配任意值,通常用于调试或实验。

4.3.1 pci_device_id 结构详解

struct pci_device_id {
    __u32 vendor, device;      // 主 ID
    __u32 subvendor, subdevice; // 子系统 ID,可设为 PCI_ANY_ID
    __u32 class, class_mask;   // 类别与掩码
    unsigned long driver_data; // 私有数据,probe 时通过 id 参数传入
};
匹配优先级与 probe 调用链 Vendor+Device Subsystem ID Class Code 通配符 pci_match_device 遍历 id_table 匹配成功 → 调用 driver->probe 匹配失败 → 继续下一个驱动
性能优化:内核将 pci_device_id 表按 vendor 排序后构建哈希表,匹配时间复杂度从 O(n) 降至 O(1)。驱动应尽量使用精确的 Vendor+Device ID,避免 Class Code 匹配带来的误命中。

4.4 probe 调用链:从内核到驱动初始化

当 pci_match_device 返回匹配的 pci_device_id 后,PCI 核心调用 pci_device_probe,该函数是 probe 调用链的入口。完整的调用链如下:

4.4.1 调用链步骤

步骤函数职责
1pci_device_probe检查设备是否已被其他驱动绑定;调用 pci_assign_resource 确保资源有效
2pci_enable_device设置 Command 寄存器(I/O、Memory、Bus Master 位);增加电源引用计数
3pci_request_regions为所有 BAR 区域申请 I/O 或 Memory 资源,防止冲突
4pci_set_master设置 Bus Master 位,允许设备发起 DMA
5driver->probe驱动自定义初始化:分配 net_device、注册中断、初始化 DMA 队列
// 驱动 probe 函数典型骨架
static int my_net_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *ndev;
    int ret;

    // 1. 启用 PCI 设备
    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

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

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

    // 4. 分配 net_device
    ndev = alloc_etherdev(sizeof(struct my_net_priv));
    if (!ndev) {
        ret = -ENOMEM;
        goto err_release;
    }
    pci_set_drvdata(pdev, ndev);
    SET_NETDEV_DEV(ndev, &pdev->dev);

    // 5. 映射 BAR 地址
    struct my_net_priv *priv = netdev_priv(ndev);
    priv->mmio_base = pci_iomap(pdev, 0, 0);
    if (!priv->mmio_base) {
        ret = -EIO;
        goto err_free_netdev;
    }

    // 6. 注册中断(MSI-X 优先)
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY);
    if (ret < 0)
        goto err_iounmap;
    priv->irq = pci_irq_vector(pdev, 0);

    // 7. 注册 net_device
    ret = register_netdev(ndev);
    if (ret)
        goto err_free_irq;

    return 0;

err_free_irq:
    pci_free_irq_vectors(pdev);
err_iounmap:
    pci_iounmap(pdev, priv->mmio_base);
err_free_netdev:
    free_netdev(ndev);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
probe 调用链完整流程 pci_device_probe pci_enable_device pci_request_regions pci_set_master driver->probe (驱动回调) alloc_etherdev pci_iomap pci_alloc_irq_vectors register_netdev probe 返回 0 → 设备就绪
最佳实践:probe 函数应遵循“失败时回滚所有已分配资源”的原则。使用 goto 标签按逆序释放资源,避免内存泄漏。内核社区推荐使用 devm_* 系列 API(如 devm_ioremap_resource)自动管理资源生命周期。

4.5 本章小结

PCI 探测流程是网络设备驱动的基石,理解其三个阶段——内核枚举、驱动注册、匹配与 probe——是编写稳定驱动的前提。关键要点:

  • 枚举阶段由 PCI 核心独立完成,驱动无需干预。
  • pci_register_driver 触发匹配,pci_match_device 按优先级遍历 ID 表。
  • probe 调用链中,pci_enable_device 和 pci_request_regions 必须在驱动自定义初始化之前完成。
  • 错误处理必须完整,确保任何失败路径都能正确释放资源。