4. NAPI 的注册与初始化

NAPI 机制的核心在于将中断驱动的收包模式转变为轮询与中断协同的模式。要实现这一转变,网络设备驱动必须在初始化阶段完成两个关键步骤:调用 netif_napi_add() 注册 NAPI 实例,以及在合适的时机调用 napi_enable() 激活该实例。本章将深入解析这两个函数的参数含义、使用规范以及调用时机,帮助你在驱动开发中正确搭建 NAPI 数据路径。

4.1 netif_napi_add() 函数详解

netif_napi_add() 是 NAPI 注册的入口函数,它将一个 napi_struct 结构体与网络设备绑定,并指定轮询回调函数。该函数定义在 <linux/netdevice.h> 中,原型如下:

void netif_napi_add(struct net_device *dev,
                    struct napi_struct *napi,
                    int (*poll)(struct napi_struct *, int),
                    int weight);

4.1.1 参数含义与使用规范

参数 类型 含义 注意事项
dev struct net_device * 指向该 NAPI 实例所属的网络设备 必须是一个已分配且有效的 net_device 实例
napi struct napi_struct * 指向待初始化的 NAPI 结构体 通常作为 net_device 私有数据的一部分,或静态分配
poll 函数指针 轮询回调函数,内核在 NAPI 调度时调用 返回值表示本次轮询处理的包数;必须小于等于 weight
weight int 每次轮询预算,即一次 poll 调用最多处理的包数 典型值为 64;对于高速网卡可适当增大,但不宜超过 256
最佳实践: weight 参数直接影响 CPU 在 NAPI 轮询中的驻留时间。weight 过小会导致频繁的调度切换,过大则可能增加网络延迟。对于千兆网卡,64 是经过验证的平衡值;对于万兆网卡,可考虑 128 或 256。

4.1.2 轮询回调函数原型

poll 回调函数是 NAPI 数据路径的核心,其原型为:

int poll_handler(struct napi_struct *napi, int budget);
  • 参数 budget:本次轮询允许处理的最大包数,由内核传入,通常等于注册时的 weight 值。
  • 返回值:实际处理的包数。当返回值小于 budget 时,内核认为队列已空,将 NAPI 切回中断模式;当返回值等于 budget 时,NAPI 保持轮询状态,等待下一次调度。
常见错误: poll 回调返回大于 budget 的值会导致内核行为异常,甚至触发 BUG_ON 检查。务必确保返回值 ≤ budget。

4.2 napi_enable() 的调用时机

napi_enable() 用于激活一个已注册的 NAPI 实例,使其能够被内核调度。函数原型简单:

void napi_enable(struct napi_struct *napi);

调用该函数后,NAPI 实例的状态从 NAPI_STATE_SCHED 未设置变为可调度状态。但激活的时机至关重要,错误的调用顺序可能导致中断丢失或数据路径混乱。

4.2.1 标准调用顺序

  1. 分配并初始化 net_device:包括设置设备名称、MAC 地址、操作函数表等。
  2. 注册 NAPI:调用 netif_napi_add() 将 napi_struct 与设备绑定。
  3. 硬件初始化:配置设备寄存器、分配 DMA 缓冲区、设置中断处理函数。
  4. 激活 NAPI:调用 napi_enable()。
  5. 启用中断:调用 netif_start_queue() 或直接操作中断使能寄存器。
关键原则: napi_enable() 必须在硬件中断使能之前调用。如果先使能中断,设备可能在 NAPI 尚未激活时触发中断,导致中断处理函数尝试调度一个未就绪的 NAPI 实例,引发空指针或状态错误。

4.2.2 典型驱动初始化代码片段

static int mydev_open(struct net_device *dev)
{
    struct mydev_priv *priv = netdev_priv(dev);
    int ret;

    /* 1. 硬件初始化:复位、配置MAC、分配RX/TX描述符环 */
    ret = mydev_hw_init(priv);
    if (ret)
        return ret;

    /* 2. 注册NAPI(通常在probe阶段已完成,此处仅为示例) */
    netif_napi_add(dev, &priv->napi, mydev_poll, 64);

    /* 3. 激活NAPI */
    napi_enable(&priv->napi);

    /* 4. 使能设备中断 */
    mydev_enable_interrupts(priv);

    /* 5. 启动发送队列 */
    netif_start_queue(dev);

    return 0;
}

4.3 NAPI 注册与激活流程总览

下图展示了从设备探测到 NAPI 就绪的完整流程,以及各阶段的关键函数调用:

NAPI 注册与激活流程 阶段1: 设备探测 alloc_etherdev() 阶段2: NAPI注册 netif_napi_add() 阶段3: 硬件初始化 DMA/描述符环配置 阶段4: NAPI激活 napi_enable() 阶段5: 中断使能 request_irq() / enable_irq() ⚠ 关键顺序约束 napi_enable() 必须在中断使能之前调用 否则设备中断触发时 NAPI 尚未就绪,可能导致内核崩溃 NAPI 状态变化 注册后 → NAPI_STATE_SCHED 未设置,不可调度 napi_enable() 后 → 可调度,等待中断触发

4.4 多队列 NAPI 注册

现代多队列网卡通常为每个 RX 队列分配独立的 NAPI 实例,以实现多核并行处理。注册方式与单队列类似,但需要为每个队列创建独立的 napi_struct:

struct mydev_priv {
    struct net_device *dev;
    struct napi_struct napi_rx[NR_CPUS];  /* 每个CPU一个NAPI实例 */
    /* ... 其他私有数据 ... */
};

static int mydev_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *dev;
    struct mydev_priv *priv;
    int i;

    dev = alloc_etherdev_mq(sizeof(*priv), NR_CPUS);
    priv = netdev_priv(dev);

    for (i = 0; i < NR_CPUS; i++) {
        netif_napi_add(dev, &priv->napi_rx[i], mydev_poll, 64);
    }

    /* ... 其余初始化 ... */
    return 0;
}
多队列注意事项: 每个 NAPI 实例的 poll 回调应只处理对应队列的包。中断处理函数中通过队列编号索引到对应的 napi_struct,然后调用 napi_schedule()。这样可以避免锁竞争,充分发挥多核性能。

4.5 常见错误与调试

错误场景 现象 根因 解决方案
napi_enable 在中断使能之后调用 系统启动后立即死锁或崩溃 中断处理函数尝试调度未激活的 NAPI 调整初始化顺序,确保 napi_enable 先于中断使能
weight 设置为 0 或负数 内核 WARN_ON 触发,NAPI 无法正常工作 内核校验 weight 必须为正数 设置 weight ≥ 1,推荐 64
poll 回调返回大于 budget NAPI 调度异常,CPU 占用率飙升 违反内核契约 确保返回值 ≤ budget
重复调用 netif_napi_add 内核告警,NAPI 列表出现重复节点 同一个 napi_struct 被多次注册 在 probe 阶段只注册一次,open 阶段只调用 napi_enable

4.6 本章小结

  • netif_napi_add() 是 NAPI 注册的核心函数,负责将 napi_struct 与设备绑定并指定轮询回调与预算。
  • napi_enable() 必须在硬件中断使能之前调用,这是 NAPI 初始化的硬性顺序约束。
  • 轮询回调的返回值必须 ≤ budget,否则会导致内核行为异常。
  • 多队列网卡应为每个 RX 队列独立注册 NAPI 实例,以实现并行处理。
  • 初始化顺序:设备分配 → NAPI 注册 → 硬件初始化 → napi_enable → 中断使能。