3. 网络驱动初始化流程

网络设备驱动的初始化是驱动生命周期中最关键的阶段,它决定了设备能否被内核正确识别、资源是否充足、以及数据通路是否畅通。本章将按照驱动加载的完整时序,逐一剖析模块加载/卸载、设备探测、资源分配和 net_device 注册四个核心环节。

3.1 驱动模块加载与卸载

Linux 网络驱动通常以内核模块的形式存在,通过 module_init 和 module_exit 宏声明入口与出口函数。加载时,内核执行初始化入口;卸载时,执行清理并释放资源。

宏 / 函数 作用 典型实现
module_init(x) 声明模块加载入口函数 static int __init mydrv_init(void)
module_exit(x) 声明模块卸载出口函数 static void __exit mydrv_exit(void)
MODULE_LICENSE("GPL") 声明模块许可证 必须声明,否则内核拒绝加载
MODULE_AUTHOR("") 声明模块作者 可选,用于调试信息
关键点: 加载函数必须使用 __init 标记,卸载函数使用 __exit 标记。这些标记告诉内核在初始化完成后释放代码段内存,节省运行态空间。
// 模块加载/卸载骨架
static int __init mydrv_init(void)
{
    printk(KERN_INFO "MyNetDrv: module loaded\n");
    // 1. 注册平台驱动或 PCI 驱动
    // 2. 探测设备
    // 3. 分配资源
    // 4. 注册 net_device
    return 0;
}

static void __exit mydrv_exit(void)
{
    printk(KERN_INFO "MyNetDrv: module unloaded\n");
    // 1. 注销 net_device
    // 2. 释放资源
    // 3. 注销驱动
}

module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");

3.2 设备探测:从总线到驱动匹配

设备探测是驱动与硬件建立联系的起点。对于 PCI 设备,驱动通过 pci_driver 结构体注册 id_table 和 probe 回调;对于平台设备,则使用 platform_driver。当总线发现设备 ID 匹配时,内核自动调用驱动的 probe 函数。

最佳实践: 在 probe 函数中尽早检查硬件是否存在、寄存器是否可访问,避免后续资源分配失败导致内核崩溃。
// PCI 驱动探测示例
static struct pci_device_id mydrv_pci_tbl[] = {
    { PCI_DEVICE(0x10EC, 0x8168) }, // Realtek 8168
    { 0, }
};
MODULE_DEVICE_TABLE(pci, mydrv_pci_tbl);

static int mydrv_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. 请求 MMIO / IO 资源
    ret = pci_request_regions(pdev, DRV_NAME);
    if (ret)
        goto err_disable;

    // 3. 分配 net_device
    ndev = alloc_etherdev(sizeof(struct mydrv_priv));
    if (!ndev) {
        ret = -ENOMEM;
        goto err_release;
    }

    // 4. 设置私有数据、回调函数
    // ...

    // 5. 注册 net_device
    ret = register_netdev(ndev);
    if (ret)
        goto err_free;

    pci_set_drvdata(pdev, ndev);
    return 0;

err_free:
    free_netdev(ndev);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}
设备探测流程(PCI 总线) 模块加载 pci_register_driver ID 匹配成功 probe() 回调 设备初始化完成 匹配依据: pci_device_id 表中的 Vendor ID + Device ID probe 失败时: 返回负错误码 驱动继续尝试其他设备

3.3 资源分配:内存、I/O 与中断

设备探测成功后,驱动必须为数据收发准备必要的系统资源。资源分配不当是驱动崩溃和性能瓶颈的主要来源。

资源类型 分配函数 释放函数 说明
MMIO / I/O 端口 pci_request_regions() pci_release_regions() 防止多个驱动操作同一硬件
DMA 缓冲区 dma_alloc_coherent() dma_free_coherent() 用于描述符环和 SKB 数据
中断 request_irq() free_irq() 注册中断处理函数
NAPI 结构 netif_napi_add() netif_napi_del() 初始化 NAPI 轮询上下文
常见陷阱: 忘记在卸载路径中按分配顺序的逆序释放资源,会导致内存泄漏、DMA 缓冲区残留、中断处理函数悬挂,严重时引发内核 panic。
// 资源分配典型代码段
static int mydrv_alloc_resources(struct mydrv_priv *priv)
{
    // 1. 分配 DMA 描述符环(Tx / Rx)
    priv->tx_ring = dma_alloc_coherent(&priv->pdev->dev,
                                        TX_RING_SIZE * sizeof(struct desc),
                                        &priv->tx_ring_dma, GFP_KERNEL);
    if (!priv->tx_ring)
        return -ENOMEM;

    priv->rx_ring = dma_alloc_coherent(&priv->pdev->dev,
                                        RX_RING_SIZE * sizeof(struct desc),
                                        &priv->rx_ring_dma, GFP_KERNEL);
    if (!priv->rx_ring)
        goto err_free_tx;

    // 2. 注册中断(支持 MSI-X 优先)
    priv->irq = pci_irq_vector(priv->pdev, 0);
    if (request_irq(priv->irq, mydrv_isr, 0, DRV_NAME, priv))
        goto err_free_rx;

    // 3. 添加 NAPI
    netif_napi_add(priv->ndev, &priv->napi, mydrv_poll, 64);

    return 0;

err_free_rx:
    dma_free_coherent(&priv->pdev->dev, RX_RING_SIZE * sizeof(struct desc),
                      priv->rx_ring, priv->rx_ring_dma);
err_free_tx:
    dma_free_coherent(&priv->pdev->dev, TX_RING_SIZE * sizeof(struct desc),
                      priv->tx_ring, priv->tx_ring_dma);
    return -ENOMEM;
}

3.4 net_device 注册:驱动与内核网络栈的桥梁

net_device 是内核网络子系统对每个网络接口的抽象。驱动必须正确填充该结构体,并通过 register_netdev() 将其纳入内核管理。注册成功后,用户空间可通过 ifconfig 或 ip 命令看到该接口。

net_device 注册流程 alloc_etherdev() 填充 net_device_ops 设置硬件特性 register_netdev() 分配 net_device + 私有数据 sizeof(struct mydrv_priv) ndev->netdev_ops = &mydrv_ops; 必须实现 ndo_start_xmit ndev->features |= NETIF_F_SG ndev->hw_features = ... 内核分配 ifindex 添加到网络命名空间
// net_device 注册完整示例
static int mydrv_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct mydrv_priv *priv;
    int ret;

    // 1. 分配 net_device + 私有数据
    ndev = alloc_etherdev(sizeof(struct mydrv_priv));
    if (!ndev)
        return -ENOMEM;

    priv = netdev_priv(ndev);
    priv->ndev = ndev;
    priv->pdev = pdev;

    // 2. 填充操作函数集
    ndev->netdev_ops = &mydrv_netdev_ops;
    ndev->ethtool_ops = &mydrv_ethtool_ops;

    // 3. 设置硬件特性(支持 TSO、SG、RXCSUM)
    ndev->hw_features = NETIF_F_SG | NETIF_F_TSO | NETIF_F_RXCSUM;
    ndev->features |= ndev->hw_features;

    // 4. 设置 MAC 地址(从 EEPROM 读取)
    // eth_hw_addr_set(ndev, mac_addr);

    // 5. 注册到内核
    ret = register_netdev(ndev);
    if (ret) {
        free_netdev(ndev);
        return ret;
    }

    pci_set_drvdata(pdev, ndev);
    return 0;
}

3.5 卸载流程:逆序清理

模块卸载时,必须严格按照初始化的逆序释放所有资源。任何遗漏都会导致内核资源泄漏,严重时影响系统稳定性。

  1. 注销 net_device:调用 unregister_netdev(ndev),将接口从内核网络栈中移除。
  2. 删除 NAPI:调用 netif_napi_del(&priv->napi),清理 NAPI 上下文。
  3. 释放中断:调用 free_irq(priv->irq, priv),注销中断处理函数。
  4. 释放 DMA 缓冲区:调用 dma_free_coherent() 释放描述符环和 SKB 数据区。
  5. 释放 PCI 资源:调用 pci_release_regions(pdev) 和 pci_disable_device(pdev)。
  6. 释放 net_device:调用 free_netdev(ndev),回收内存。
调试建议: 在卸载函数中增加 printk 日志,标记每个清理步骤的完成状态。如果卸载时系统卡死或报错,日志可快速定位未释放的资源。
// 模块卸载函数
static void __exit mydrv_exit(void)
{
    struct net_device *ndev = pci_get_drvdata(pdev);
    struct mydrv_priv *priv = netdev_priv(ndev);

    // 1. 注销 net_device
    unregister_netdev(ndev);

    // 2. 删除 NAPI
    netif_napi_del(&priv->napi);

    // 3. 释放中断
    free_irq(priv->irq, priv);

    // 4. 释放 DMA 缓冲区
    dma_free_coherent(&priv->pdev->dev, TX_RING_SIZE * sizeof(struct desc),
                      priv->tx_ring, priv->tx_ring_dma);
    dma_free_coherent(&priv->pdev->dev, RX_RING_SIZE * sizeof(struct desc),
                      priv->rx_ring, priv->rx_ring_dma);

    // 5. 释放 PCI 资源
    pci_release_regions(priv->pdev);
    pci_disable_device(priv->pdev);

    // 6. 释放 net_device
    free_netdev(ndev);
}

3.6 初始化流程全景图

将上述四个环节串联起来,形成完整的驱动初始化生命周期。理解这张图,就掌握了网络驱动加载的核心脉络。

网络驱动初始化完整生命周期 模块加载 module_init 设备探测 probe() 资源分配 DMA / IRQ / NAPI net_device 注册 register_netdev 卸载流程(逆序) 注销 net_device unregister_netdev 释放资源 DMA / IRQ / NAPI 释放 PCI 资源 pci_release_regions 模块卸载 module_exit 核心原则: 1. 加载时:先分配资源,后注册设备 2. 卸载时:先注销设备,后释放资源 3. 任何一步失败,必须回滚已分配的所有资源

3.7 常见错误与调试技巧

错误场景 现象 解决方案
probe 中未检查 pci_enable_device 返回值 设备无法访问,寄存器读写异常 始终检查返回值,失败时提前返回
DMA 缓冲区大小未对齐 硬件 DMA 错误,数据损坏 使用 dma_set_coherent_mask 确保对齐
中断注册在资源分配之前 中断触发时访问未初始化的数据结构 严格遵循:资源分配 → 中断注册
卸载时遗漏 netif_napi_del NAPI 残留,下次加载时冲突 在 unregister_netdev 后立即删除 NAPI
调试利器: 使用 dev_info() / dev_err() 替代 printk,它们会自动附加设备名称,让日志更易读。配合 dmesg -w 实时观察驱动加载过程。