3. NAPI 初始化与注册:核心 API 解析

NAPI 机制的启动始于驱动初始化阶段。驱动必须完成两个关键步骤:调用 netif_napi_add() 将 NAPI 实例注册到内核,再通过 napi_enable() 激活轮询能力。本节将深入解析这三个 API 的语义、参数、使用场景及常见陷阱。

3.1 netif_napi_add():NAPI 实例的注册

netif_napi_add() 是 NAPI 初始化的入口函数,负责分配并初始化 struct napi_struct,将其挂入网络设备的 NAPI 链表。该函数通常在驱动 probe() 或 open() 回调中调用。

函数原型与参数

void netif_napi_add(struct net_device *dev,
                    struct napi_struct *napi,
                    int (*poll)(struct napi_struct *, int),
                    int weight);
参数 类型 说明
dev struct net_device * 所属网络设备实例,NAPI 与该设备绑定
napi struct napi_struct * 驱动预先分配的 NAPI 实例指针(通常嵌入私有结构体)
poll 函数指针 轮询回调函数,内核在软中断上下文中调用
weight int 单次轮询最大处理报文数,典型值 64
关键约束:weight 值不能超过 NAPI_POLL_WEIGHT(内核默认 64)。过大的 weight 会导致软中断独占 CPU,造成其他任务饥饿。对于高速网卡(如 25G/100G),建议保持 64 或根据实测调低。

内部数据结构关系

调用 netif_napi_add() 后,内核完成以下操作:

  • 初始化 napi->poll 和 napi->weight
  • 将 napi 加入 dev->napi_list 双向链表
  • 设置 napi->dev 回指设备
  • 初始化 napi->poll_list,用于挂入 CPU 的轮询队列
  • 清除 NAPI_STATE_SCHED 标志位
驱动最佳实践:将 struct napi_struct 嵌入驱动私有结构体(如 struct my_adapter),而非单独分配。这样可保证 NAPI 实例与硬件队列一一对应,便于多队列扩展。
struct my_adapter {
    struct net_device *netdev;
    struct napi_struct rx_napi;   // 嵌入 NAPI 实例
    // ... 其他成员
};

static int my_probe(struct pci_dev *pdev, ...)
{
    struct my_adapter *adapter;
    // ... 分配 adapter 和 netdev

    netif_napi_add(adapter->netdev, &adapter->rx_napi,
                   my_poll, 64);
    return 0;
}

3.2 NAPI 注册流程与数据结构

下图展示了 netif_napi_add() 执行后,NAPI 实例与内核子系统之间的静态关系:

netif_napi_add() 注册后的数据结构关系 struct net_device napi_list: <链表头> features: NETIF_F_GRO priv: &adapter struct napi_struct poll = my_poll weight = 64 dev = &netdev state = 0 (未调度) struct my_adapter rx_napi (嵌入) rx_ring: <DMA> irq_num: 42 napi_list 节点 嵌入关系 软中断上下文 (ksoftirqd/CPU) napi->poll_list 挂入当前 CPU 的 poll_queue 硬件中断处理函数 (ISR) napi_schedule() → 将 napi 加入 poll_list → 触发 NET_RX_SOFTIRQ

3.3 napi_enable() 与 napi_disable():生命周期管理

注册 NAPI 实例后,驱动需要通过 napi_enable() 和 napi_disable() 控制其运行状态。这两个函数管理 NAPI_STATE_SCHED 和 NAPI_STATE_DISABLE 标志位,确保轮询与硬件操作同步。

函数语义对比

函数 原型 核心行为 调用时机
napi_enable() void napi_enable(struct napi_struct *n) 清除 NAPI_STATE_DISABLE 标志,允许 napi_schedule() 将实例加入轮询队列 probe() 末尾、open() 中、链路恢复后
napi_disable() void napi_disable(struct napi_struct *n) 设置 NAPI_STATE_DISABLE,等待当前轮询退出,禁止新调度 close()、suspend()、移除设备前
重要:napi_disable() 是 阻塞调用。如果当前 CPU 正在执行该 NAPI 实例的 poll 回调,napi_disable() 会忙等待直到 poll 返回。因此,绝不能在 poll 回调自身或持有自旋锁的上下文中调用 napi_disable(),否则会导致死锁。

典型生命周期序列

  1. probe():分配资源 → netif_napi_add() → napi_enable()
  2. open():开启硬件中断 → napi_enable()(若 probe 时未启用)
  3. 正常运行:ISR 调用 napi_schedule() → 软中断执行 poll
  4. close():关闭硬件中断 → napi_disable() → 释放资源
  5. remove():napi_disable() → netif_napi_del() → 释放内存

驱动代码示例

static int my_open(struct net_device *netdev)
{
    struct my_adapter *adapter = netdev_priv(netdev);

    // 1. 开启硬件中断
    my_irq_enable(adapter);

    // 2. 启用 NAPI
    napi_enable(&adapter->rx_napi);

    // 3. 启动 TX 队列
    netif_tx_start_all_queues(netdev);
    return 0;
}

static int my_close(struct net_device *netdev)
{
    struct my_adapter *adapter = netdev_priv(netdev);

    // 1. 停止 TX 队列
    netif_tx_stop_all_queues(netdev);

    // 2. 禁用 NAPI(阻塞,等待 poll 退出)
    napi_disable(&adapter->rx_napi);

    // 3. 关闭硬件中断
    my_irq_disable(adapter);
    return 0;
}

3.4 napi_enable/napi_disable 使用场景对照

场景 操作 原因
设备 probe 完成 napi_enable() 使 NAPI 实例可被调度,准备接收数据
接口 up (open) napi_enable() 确保中断开启前 NAPI 已就绪
接口 down (close) napi_disable() 防止中断触发调度,安全停止轮询
系统 suspend napi_disable() 设备进入低功耗,禁止 DMA 和中断
系统 resume napi_enable() 恢复设备后重新激活 NAPI
链路 down (LSC) napi_disable() 避免无数据时空转轮询
链路 up (LSC) napi_enable() 恢复数据接收能力
设备移除 (remove) napi_disable() + netif_napi_del() 完整清理 NAPI 实例

3.5 常见错误与避坑指南

错误 1:在 napi_disable() 之后调用 napi_schedule()。
后果:调度请求被忽略,数据包丢失。
修复:确保中断处理函数中先检查 NAPI_STATE_DISABLE 标志。
错误 2:多次调用 napi_enable() 而不配对 napi_disable()。
后果:引用计数混乱,可能导致 NAPI 永远无法禁用。
修复:严格遵循 enable/disable 配对调用。
错误 3:在 poll 回调中调用 napi_disable()。
后果:死锁——napi_disable() 等待 poll 返回,而 poll 在等待 disable 完成。
修复:使用工作队列或 tasklet 延迟执行 disable 操作。

3.6 多队列场景下的 NAPI 初始化

现代网卡支持多个 RX 队列,每个队列应拥有独立的 NAPI 实例。初始化模式如下:

struct my_adapter {
    struct net_device *netdev;
    struct my_ring *rx_ring;
    int num_rx_queues;
    struct napi_struct *napi;  // 动态数组
};

static int my_probe(struct pci_dev *pdev, ...)
{
    struct my_adapter *adapter;
    int i;

    adapter->napi = kcalloc(adapter->num_rx_queues,
                            sizeof(struct napi_struct), GFP_KERNEL);

    for (i = 0; i < adapter->num_rx_queues; i++) {
        netif_napi_add(adapter->netdev, &adapter->napi[i],
                       my_poll, 64);
        napi_enable(&adapter->napi[i]);
    }
    return 0;
}

每个 NAPI 实例绑定到特定 RX 队列的中断,实现 RSS(接收端缩放)与 NAPI 的协同工作。

3.7 本章小结

  • netif_napi_add() 注册 NAPI 实例,指定 poll 回调和 weight 权重
  • napi_enable() 激活 NAPI,允许中断触发调度
  • napi_disable() 阻塞禁用 NAPI,确保轮询安全停止
  • 多队列场景下每个 RX 队列独立 NAPI 实例
  • 严格遵循 enable/disable 配对,避免在 poll 中调用 disable