4. 网络设备打开与关闭
网络设备的打开(open)与关闭(stop)是驱动生命周期中最关键的两个状态转换点。当用户执行 ifconfig eth0 up 或 ip link set eth0 up 时,内核最终会调用驱动注册的 ndo_open 回调;反之,ifconfig eth0 down 触发 ndo_stop。这两个回调函数承担着硬件资源申请、中断注册、NAPI 初始化、队列创建与销毁等核心任务。
4.1 open回调函数:设备启动的完整流程
open 回调是设备从“静默”状态进入“可收发数据”状态的入口。一个典型的 open 实现需要完成以下步骤:
- 硬件复位与初始化:重置 MAC 控制器、PHY 芯片,设置工作模式(全双工/半双工、流控等)。
- 中断注册:调用
request_irq()或request_threaded_irq()注册中断处理函数。 - NAPI 初始化:调用
netif_napi_add()将 NAPI 结构体添加到网络设备中。 - 队列初始化:分配并初始化 TX/RX 描述符环形缓冲区(Ring Buffer)。
- 启动硬件:使能 MAC 发送/接收引擎,开启中断。
- 启动队列:调用
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 的逆操作,必须确保设备完全停止收发,并释放所有已分配的资源。其典型流程包括:
- 停止发送队列:调用
netif_stop_queue()阻止上层继续投递数据包。 - 关闭硬件:禁用 MAC 发送/接收引擎,屏蔽中断。
- 释放中断:调用
free_irq()注销中断处理函数。 - 删除 NAPI:调用
netif_napi_del()从设备中移除 NAPI 结构体。 - 清理描述符队列:释放 TX/RX 环形缓冲区及关联的 SKB。
- 硬件复位(可选):将 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 多队列中断,则需要为每个队列注册独立的中断处理函数。
napi_schedule() 触发对应队列的轮询,实现负载均衡。
4.4 队列初始化:描述符环形缓冲区
描述符环形缓冲区(Ring Buffer)是驱动与硬件之间交换数据包元数据的核心数据结构。每个 TX 或 RX 队列都对应一个环形缓冲区,其中每个条目(Descriptor)包含数据缓冲区的物理地址、长度、状态标志等信息。
初始化一个 RX 环形缓冲区通常包括以下步骤:
- 分配 DMA 一致性内存:使用
dma_alloc_coherent()为描述符数组分配物理连续且 DMA 可访问的内存。 - 分配 SKB 并映射 DMA 地址:为每个描述符分配
struct sk_buff,将其数据缓冲区的物理地址写入描述符。 - 设置描述符所有权:将描述符的所有权标记为“硬件所有”,表示硬件可以写入数据。
- 初始化指针:设置生产者索引(硬件写入位置)和消费者索引(驱动读取位置)为 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 在其中的角色:
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);
}
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 观察中断注册是否正确。