4. ndo_open 与 ndo_stop 接口:设备启动与停止的实现、中断使能与关闭

网络设备驱动中,ndo_open 和 ndo_stop 是 struct net_device_ops 中最核心的两个回调。它们分别对应设备的上线(UP)与下线(DOWN)操作,是数据包收发通道的起点和终点。本章将深入剖析这两个接口的实现要点,重点讲解中断使能/关闭、NAPI 初始化、队列激活等关键步骤。

4.1 ndo_open:设备启动的完整链路

当用户执行 ip link set eth0 up 或系统自动激活设备时,内核网络核心层会调用驱动注册的 ndo_open 回调。该函数必须完成以下核心任务:

  1. 硬件复位与初始化:重置 MAC 控制器、PHY 状态机,配置基本寄存器。
  2. 中断使能:注册并启用设备的中断处理函数,通常包括 TX/RX 完成中断、错误中断等。
  3. NAPI 初始化:调用 netif_napi_add() 注册 NAPI 结构体,并默认处于禁用状态。
  4. 描述符环初始化:分配并初始化 TX/RX 环形缓冲区(Ring Buffer),设置 DMA 地址。
  5. 启动 NAPI:调用 napi_enable() 使能 NAPI 调度。
  6. 通知内核:调用 netif_carrier_on() 和 netif_tx_start_all_queues() 宣告设备就绪。
关键原则:中断使能必须在 NAPI 使能之前完成,否则可能出现中断触发时 NAPI 尚未准备好的竞态条件。

4.2 ndo_stop:设备停止的逆序操作

ndo_stop 是 ndo_open 的逆过程,必须安全地关闭所有正在进行的收发活动:

  1. 停止 TX 队列:调用 netif_tx_stop_all_queues() 阻止内核继续发送数据包。
  2. 关闭 NAPI:调用 napi_disable() 确保 NAPI 调度循环完全退出。
  3. 关闭中断:屏蔽设备所有中断源,释放 IRQ(如果使用共享中断)。
  4. 回收描述符:清理 TX/RX 环中残留的 SKB,释放 DMA 映射。
  5. 硬件下电:将 MAC/PHY 置于低功耗或复位状态。
  6. 通知内核:调用 netif_carrier_off() 清除链路状态。
常见陷阱:在 ndo_stop 中直接调用 free_irq() 可能导致正在执行的中断上下文访问已释放的数据结构。正确的做法是先屏蔽中断,再同步等待所有中断处理完成(如使用 synchronize_irq()),最后释放资源。

4.3 中断使能与关闭的典型实现模式

中断控制是 ndo_open/stop 的核心环节。以下表格对比了两种常见的中断管理策略:

策略 使能方式 关闭方式 适用场景
单中断模式 写中断使能寄存器,调用 request_irq() 写中断屏蔽寄存器,调用 free_irq() 简单设备,中断源较少
MSI-X 多队列模式 为每个队列分配独立中断向量,分别使能 逐个屏蔽队列中断,释放 MSI-X 向量 高性能多队列网卡(如 Intel ixgbe)

以下代码片段展示了 ndo_open 中中断使能与 NAPI 初始化的典型顺序:

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

    /* 1. 硬件初始化 */
    my_hw_reset(priv);

    /* 2. 分配描述符环 */
    ret = my_alloc_rings(priv);
    if (ret)
        return ret;

    /* 3. 注册中断(使能中断) */
    ret = request_irq(priv->irq, my_interrupt, 0, ndev->name, ndev);
    if (ret) {
        my_free_rings(priv);
        return ret;
    }

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

    /* 5. 使能 NAPI */
    napi_enable(&priv->napi);

    /* 6. 启动 TX 队列并宣告链路就绪 */
    netif_tx_start_all_queues(ndev);
    netif_carrier_on(ndev);

    return 0;
}

对应的 ndo_stop 实现:

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

    /* 1. 停止 TX 队列 */
    netif_tx_stop_all_queues(ndev);
    netif_carrier_off(ndev);

    /* 2. 关闭 NAPI(确保 poll 退出) */
    napi_disable(&priv->napi);

    /* 3. 屏蔽中断并释放 IRQ */
    my_irq_disable(priv);
    synchronize_irq(priv->irq);
    free_irq(priv->irq, ndev);

    /* 4. 清理 NAPI 和描述符 */
    netif_napi_del(&priv->napi);
    my_free_rings(priv);

    /* 5. 硬件下电 */
    my_hw_power_down(priv);

    return 0;
}

4.4 设备启动与停止的状态机

下图展示了 ndo_open 和 ndo_stop 之间的状态转换关系,以及中断与 NAPI 的生命周期:

设备启动与停止状态机 设备启动/停止状态机与中断/NAPI 生命周期 DOWN 中断关闭 / NAPI 未注册 INIT 硬件复位 / 环分配 UP 中断使能 / NAPI 运行 STOPPING NAPI 禁用 / 中断屏蔽 ndo_open 中断使能 + NAPI 使能 ndo_stop 资源释放完成 ⚠ 关键时序约束 中断使能 → NAPI 使能 NAPI 禁用 → 中断屏蔽

4.5 中断使能/关闭的寄存器操作细节

实际硬件的中断控制通常涉及以下寄存器位操作:

  • 中断状态寄存器(ISR):只读,用于判断中断来源。
  • 中断屏蔽寄存器(IMR):写 1 屏蔽对应中断,写 0 使能。
  • 中断使能寄存器(IER):写 1 使能对应中断源。
最佳实践:在 ndo_open 中使能中断前,先清除所有挂起的中断状态位,避免旧中断立即触发。在 ndo_stop 中屏蔽中断后,建议读取一次 ISR 以确认无残留中断。

4.6 多队列场景下的扩展考虑

对于多队列网卡,每个队列通常拥有独立的 NAPI 实例和中断向量。ndo_open 需要遍历所有队列:

for (i = 0; i < priv->num_queues; i++) {
    struct my_queue *q = &priv->queues[i];
    netif_napi_add(ndev, &q->napi, my_poll, 64);
    napi_enable(&q->napi);
    /* 使能该队列的中断 */
    my_enable_queue_irq(priv, i);
}

对应的 ndo_stop 则逆序操作:

for (i = 0; i < priv->num_queues; i++) {
    struct my_queue *q = &priv->queues[i];
    my_disable_queue_irq(priv, i);
    napi_disable(&q->napi);
    netif_napi_del(&q->napi);
}

4.7 常见问题与调试要点

问题现象 可能原因 排查方法
设备无法 UP 中断注册失败、DMA 分配失败 检查 dmesg 错误码,确认 request_irq 返回值
UP 后立即丢包 NAPI 使能前中断已触发 在 napi_enable 前先屏蔽中断
DOWN 时系统卡死 napi_disable 等待 poll 未退出 检查 poll 函数是否在 napi_complete 后仍有重调度
中断风暴 中断未正确屏蔽或共享中断冲突 使用 cat /proc/interrupts 观察中断计数

掌握 ndo_open 与 ndo_stop 的正确实现,是构建稳定网络设备驱动的基石。中断与 NAPI 的使能顺序、资源释放的完整性,直接决定了驱动在热插拔、链路抖动等场景下的健壮性。