4. 网络设备打开与关闭

网络设备的打开(open)与关闭(stop)是驱动生命周期中最关键的两个状态转换点。当用户执行 ifconfig eth0 up 或 ip link set eth0 up 时,内核最终会调用驱动注册的 ndo_open 回调;反之,ifconfig eth0 down 触发 ndo_stop。这两个回调函数承担着硬件资源申请、中断注册、NAPI 初始化、队列创建与销毁等核心任务。

4.1 open回调函数:设备启动的完整流程

open 回调是设备从“静默”状态进入“可收发数据”状态的入口。一个典型的 open 实现需要完成以下步骤:

  1. 硬件复位与初始化:重置 MAC 控制器、PHY 芯片,设置工作模式(全双工/半双工、流控等)。
  2. 中断注册:调用 request_irq() 或 request_threaded_irq() 注册中断处理函数。
  3. NAPI 初始化:调用 netif_napi_add() 将 NAPI 结构体添加到网络设备中。
  4. 队列初始化:分配并初始化 TX/RX 描述符环形缓冲区(Ring Buffer)。
  5. 启动硬件:使能 MAC 发送/接收引擎,开启中断。
  6. 启动队列:调用 netif_start_queue() 或 netif_tx_start_all_queues() 允许上层开始发送数据包。
关键原则:open 必须保证所有资源在成功时全部就绪,在失败时全部回滚。任何中间步骤失败都应调用已成功步骤的逆操作,并返回负的错误码(如 -ENOMEM、-EIO)。

以下是一个简化的 open 回调代码示例:

static int my_netdev_open(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    int ret;

    /* 1. 硬件复位 */
    my_hw_reset(priv);

    /* 2. 分配并初始化描述符队列 */
    ret = my_init_rx_ring(priv);
    if (ret)
        goto err_rx_ring;
    ret = my_init_tx_ring(priv);
    if (ret)
        goto err_tx_ring;

    /* 3. 注册中断 */
    ret = request_irq(priv->irq, my_interrupt_handler, 0,
                      ndev->name, ndev);
    if (ret)
        goto err_irq;

    /* 4. 初始化 NAPI */
    netif_napi_add(ndev, &priv->napi, my_poll, 64);

    /* 5. 使能硬件发送/接收 */
    my_hw_enable(priv);

    /* 6. 启动发送队列 */
    netif_start_queue(ndev);

    return 0;

err_irq:
    my_free_tx_ring(priv);
err_tx_ring:
    my_free_rx_ring(priv);
err_rx_ring:
    return ret;
}

4.2 stop回调函数:优雅关闭与资源释放

stop 回调是 open 的逆操作,必须确保设备完全停止收发,并释放所有已分配的资源。其典型流程包括:

  1. 停止发送队列:调用 netif_stop_queue() 阻止上层继续投递数据包。
  2. 关闭硬件:禁用 MAC 发送/接收引擎,屏蔽中断。
  3. 释放中断:调用 free_irq() 注销中断处理函数。
  4. 删除 NAPI:调用 netif_napi_del() 从设备中移除 NAPI 结构体。
  5. 清理描述符队列:释放 TX/RX 环形缓冲区及关联的 SKB。
  6. 硬件复位(可选):将 PHY 和 MAC 置于低功耗或复位状态。
注意:在 stop 中调用 free_irq() 之前,必须确保中断处理函数不会再被调用。通常先禁用硬件中断,再调用 synchronize_irq() 等待正在执行的中断处理函数完成。
static int my_netdev_stop(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 1. 停止发送队列 */
    netif_stop_queue(ndev);

    /* 2. 禁用硬件中断和收发引擎 */
    my_hw_disable(priv);

    /* 3. 同步中断,确保处理函数退出 */
    synchronize_irq(priv->irq);

    /* 4. 释放中断 */
    free_irq(priv->irq, ndev);

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

    /* 6. 释放描述符队列 */
    my_free_tx_ring(priv);
    my_free_rx_ring(priv);

    return 0;
}

4.3 中断注册:选择合适的中断处理模型

Linux 内核提供多种中断注册接口,驱动开发者需要根据硬件特性选择最合适的模型:

接口函数 适用场景 特点
request_irq() 传统中断,处理时间短 中断上下文执行,不能睡眠
request_threaded_irq() 需要睡眠操作的中断 分为硬中断 handler 和线程化 handler
request_any_context_irq() 不确定中断上下文 自动选择普通中断或线程化中断

对于网络设备,推荐使用 request_irq() 配合 NAPI 机制。中断处理函数只做最少的硬件操作(如读取状态寄存器、禁用中断),然后通过 NAPI 调度轮询处理。如果硬件支持 MSI-X 多队列中断,则需要为每个队列注册独立的中断处理函数。

最佳实践:在多队列网卡驱动中,每个 RX 队列通常拥有独立的中断号和 NAPI 实例。中断处理函数中调用 napi_schedule() 触发对应队列的轮询,实现负载均衡。

4.4 队列初始化:描述符环形缓冲区

描述符环形缓冲区(Ring Buffer)是驱动与硬件之间交换数据包元数据的核心数据结构。每个 TX 或 RX 队列都对应一个环形缓冲区,其中每个条目(Descriptor)包含数据缓冲区的物理地址、长度、状态标志等信息。

初始化一个 RX 环形缓冲区通常包括以下步骤:

  1. 分配 DMA 一致性内存:使用 dma_alloc_coherent() 为描述符数组分配物理连续且 DMA 可访问的内存。
  2. 分配 SKB 并映射 DMA 地址:为每个描述符分配 struct sk_buff,将其数据缓冲区的物理地址写入描述符。
  3. 设置描述符所有权:将描述符的所有权标记为“硬件所有”,表示硬件可以写入数据。
  4. 初始化指针:设置生产者索引(硬件写入位置)和消费者索引(驱动读取位置)为 0。
static int my_init_rx_ring(struct my_priv *priv)
{
    int i;

    /* 分配描述符数组 */
    priv->rx_ring = dma_alloc_coherent(priv->dev,
                     RX_RING_SIZE * sizeof(struct my_desc),
                     &priv->rx_ring_dma, GFP_KERNEL);
    if (!priv->rx_ring)
        return -ENOMEM;

    /* 为每个描述符分配 SKB 并映射 DMA */
    for (i = 0; i < RX_RING_SIZE; i++) {
        struct sk_buff *skb = netdev_alloc_skb(priv->ndev, RX_BUF_SIZE);
        if (!skb)
            goto err_free_skbs;

        priv->rx_skb[i] = skb;
        priv->rx_ring[i].addr = dma_map_single(priv->dev,
                                  skb->data, RX_BUF_SIZE,
                                  DMA_FROM_DEVICE);
        priv->rx_ring[i].len = RX_BUF_SIZE;
        priv->rx_ring[i].flags = DESC_OWN_HW;  /* 所有权交给硬件 */
    }

    priv->rx_cur = 0;  /* 消费者索引 */
    return 0;

err_free_skbs:
    while (--i >= 0) {
        dma_unmap_single(priv->dev, priv->rx_ring[i].addr,
                         RX_BUF_SIZE, DMA_FROM_DEVICE);
        dev_kfree_skb(priv->rx_skb[i]);
    }
    dma_free_coherent(priv->dev,
                      RX_RING_SIZE * sizeof(struct my_desc),
                      priv->rx_ring, priv->rx_ring_dma);
    return -ENOMEM;
}

4.5 open/stop 状态机与 NAPI 集成

下图展示了从设备关闭到打开,再到关闭的完整状态转换,以及 NAPI 在其中的角色:

网络设备 open/stop 状态机与 NAPI 集成 设备关闭 ndo_stop 完成 ndo_open 调用 硬件复位/资源分配 中断注册 request_irq() NAPI 初始化 netif_napi_add() 队列初始化 Ring Buffer 分配 设备打开 netif_start_queue() ndo_stop 调用 停止队列/关闭硬件 中断释放 free_irq() NAPI 删除 netif_napi_del() 说明:打开流程自上而下顺序执行,任何一步失败需回滚;关闭流程逆序执行,确保资源完全释放。

4.6 多队列场景下的扩展

现代网卡支持多 TX/RX 队列,每个队列拥有独立的描述符环形缓冲区和中断。在 open 回调中,需要为每个队列重复执行初始化操作:

组件 单队列 多队列
描述符环形缓冲区 1 个 TX Ring + 1 个 RX Ring N 个 TX Ring + N 个 RX Ring
中断处理函数 1 个 handler 每个队列 1 个 handler(或共享)
NAPI 实例 1 个 napi_struct 每个 RX 队列 1 个 napi_struct
发送队列控制 netif_start_queue() netif_tx_start_all_queues()

多队列初始化代码通常使用循环结构:

for (i = 0; i < priv->num_queues; i++) {
    /* 初始化第 i 个 RX 队列 */
    my_init_rx_queue(priv, i);
    /* 初始化第 i 个 TX 队列 */
    my_init_tx_queue(priv, i);
    /* 注册第 i 个中断 */
    request_irq(priv->irq[i], my_queue_handler, 0,
                ndev->name, &priv->queue[i]);
    /* 添加第 i 个 NAPI */
    netif_napi_add(ndev, &priv->napi[i], my_poll, 64);
}
性能提示:多队列初始化时,建议将每个队列的 NAPI 实例绑定到不同的 CPU 核心(通过 netif_set_xps_queue() 和 irq_set_affinity_hint()),实现中断负载均衡,提升多核处理器的吞吐量。

4.7 常见错误与调试

在 open/stop 实现中,以下错误最为常见:

  • 资源泄漏:open 中途失败时未释放已分配的 DMA 内存或 SKB,导致内存泄漏。
  • 中断风暴:中断注册后未正确屏蔽硬件中断,导致设备在无数据时持续产生中断。
  • NAPI 重复添加:多次调用 netif_napi_add() 而未先调用 netif_napi_del(),导致内核崩溃。
  • DMA 映射错误:描述符中写入的 DMA 地址与 SKB 数据缓冲区不一致,导致硬件访问非法内存。
  • 竞态条件:stop 执行期间中断处理函数仍在运行,未使用 synchronize_irq() 同步。
调试建议:在 open 和 stop 中添加 netdev_dbg() 打印关键步骤的返回值;使用 CONFIG_DMA_API_DEBUG 内核选项检测 DMA 映射错误;通过 /proc/interrupts 观察中断注册是否正确。