网络设备驱动开发:从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 Number | 8 bit | 0~255,最多 256 条总线 |
| Device Number | 5 bit | 0~31,每条总线最多 32 个设备 |
| Function Number | 3 bit | 0~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 到叶子节点
- 扫描 Root Bridge:ACPI 或 Device Tree 告知系统主桥的配置空间基址。
- 遍历总线 0:读取每个 Device Function 0 的 Vendor ID。
- 检测 PCI-PCI Bridge:若设备 Class Code 为 0x060400,则读取其 Secondary Bus Number 寄存器,递归扫描下级总线。
- 分配资源:为每个设备分配 MMIO、I/O 端口、IRQ 等资源,写入配置空间的 BAR 寄存器。
- 创建 pci_dev:将设备加入全局链表
pci_devices,并触发pci_bus_add_device。
避坑提示: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 核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
name | const char * | 驱动名称,用于 sysfs 和调试 |
id_table | struct pci_device_id * | 设备 ID 表,定义支持的 Vendor/Device/Subsystem/Class |
probe | int (*)(struct pci_dev *, const struct pci_device_id *) | 设备匹配成功后的初始化回调 |
remove | void (*)(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 是驱动与设备之间的“红娘”。它按照以下优先级顺序检查:
- Vendor ID + Device ID:最精确匹配,使用
PCI_DEVICE(vendor, device)宏。 - Subsystem Vendor + Subsystem Device:用于区分同一芯片的不同板卡版本。
- Class Code:匹配设备类别(如网络控制器 Class=0x020000),用于通用驱动。
- 通配符:
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 参数传入
};
性能优化:内核将
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 调用链步骤
| 步骤 | 函数 | 职责 |
|---|---|---|
| 1 | pci_device_probe | 检查设备是否已被其他驱动绑定;调用 pci_assign_resource 确保资源有效 |
| 2 | pci_enable_device | 设置 Command 寄存器(I/O、Memory、Bus Master 位);增加电源引用计数 |
| 3 | pci_request_regions | 为所有 BAR 区域申请 I/O 或 Memory 资源,防止冲突 |
| 4 | pci_set_master | 设置 Bus Master 位,允许设备发起 DMA |
| 5 | driver->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 函数应遵循“失败时回滚所有已分配资源”的原则。使用
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必须在驱动自定义初始化之前完成。 - 错误处理必须完整,确保任何失败路径都能正确释放资源。