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 回调。该函数必须完成以下核心任务:
- 硬件复位与初始化:重置 MAC 控制器、PHY 状态机,配置基本寄存器。
- 中断使能:注册并启用设备的中断处理函数,通常包括 TX/RX 完成中断、错误中断等。
- NAPI 初始化:调用
netif_napi_add()注册 NAPI 结构体,并默认处于禁用状态。 - 描述符环初始化:分配并初始化 TX/RX 环形缓冲区(Ring Buffer),设置 DMA 地址。
- 启动 NAPI:调用
napi_enable()使能 NAPI 调度。 - 通知内核:调用
netif_carrier_on()和netif_tx_start_all_queues()宣告设备就绪。
关键原则:中断使能必须在 NAPI 使能之前完成,否则可能出现中断触发时 NAPI 尚未准备好的竞态条件。
4.2 ndo_stop:设备停止的逆序操作
ndo_stop 是 ndo_open 的逆过程,必须安全地关闭所有正在进行的收发活动:
- 停止 TX 队列:调用
netif_tx_stop_all_queues()阻止内核继续发送数据包。 - 关闭 NAPI:调用
napi_disable()确保 NAPI 调度循环完全退出。 - 关闭中断:屏蔽设备所有中断源,释放 IRQ(如果使用共享中断)。
- 回收描述符:清理 TX/RX 环中残留的 SKB,释放 DMA 映射。
- 硬件下电:将 MAC/PHY 置于低功耗或复位状态。
- 通知内核:调用
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 的生命周期:
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 的使能顺序、资源释放的完整性,直接决定了驱动在热插拔、链路抖动等场景下的健壮性。