3. NAPI 的初始化与注册

NAPI 机制的启用始于驱动初始化阶段。在设备被内核发现并分配 net_device 结构体之后,驱动必须在 probe 回调中完成 NAPI 实例的创建与注册。本章将深入剖析 netif_napi_add() 函数的原型、参数含义、注册时机以及多队列场景下的注册策略。

3.1 netif_napi_add() 函数原型与参数详解

netif_napi_add() 是内核提供给驱动注册 NAPI 实例的核心 API,定义在 <linux/netdevice.h> 中。其函数原型如下:

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 实例所属的网络设备 必须已分配且非 NULL;多队列场景下所有 NAPI 实例共享同一个 dev
napi struct napi_struct * 指向待初始化的 NAPI 实例结构体 通常嵌入在驱动私有数据结构中,由 kzalloc 分配或静态定义
poll int (*)(struct napi_struct *, int) 轮询回调函数,负责从硬件接收队列中读取数据并上送协议栈 返回值表示本次轮询处理的报文数量;必须小于等于 weight
weight int 单次轮询预算,即 poll 回调最多可处理的报文数 典型值为 64;对于纯 NAPI 驱动,weight 不应超过 NAPI_POLL_WEIGHT (64)
关键约束: weight 参数决定了单次软中断上下文中驱动最多能消耗的 CPU 时间片。设置过大会导致其他网络流被饿死,设置过小则降低收包吞吐量。内核社区推荐值为 64,该值在吞吐量与公平性之间取得了良好平衡。

调用 netif_napi_add() 后,内核会完成以下初始化工作:

  • 将 napi->dev 指向传入的 dev;
  • 将 napi->poll 和 napi->weight 赋值为传入参数;
  • 初始化 NAPI 实例的链表节点、状态标志位和就绪队列;
  • 将 NAPI 实例添加到设备 dev->napi_list 链表中,完成注册。
编程提示: 在调用 netif_napi_add() 之前,驱动应确保 napi_struct 结构体已被清零(推荐使用 kzalloc 分配),否则未初始化的链表指针可能导致内核崩溃。
netif_napi_add 内部初始化流程 netif_napi_add() 内部初始化流程 dev napi_struct * poll 回调 weight napi->dev = dev napi->poll = poll; napi->weight = weight 初始化链表节点 & 状态标志 添加到 dev->napi_list

3.2 NAPI 实例的注册时机:驱动 probe 阶段

NAPI 实例的注册必须发生在设备初始化流程的 probe 阶段,即在 struct net_device_ops 中的 ndo_open 被调用之前。典型的注册时序如下:

  1. 分配 net_device:调用 alloc_etherdev() 或 alloc_netdev() 分配网络设备结构体。
  2. 设置硬件参数:读取 PCI 配置空间、映射 BAR 空间、初始化 DMA 队列等。
  3. 分配驱动私有数据:在私有结构中为每个 RX 队列分配 napi_struct 实例。
  4. 调用 netif_napi_add():为每个 RX 队列注册 NAPI 实例。
  5. 注册 net_device:调用 register_netdev() 将设备注册到内核网络子系统。

以下是一个典型的驱动 probe 函数片段:

static int mydrv_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct mydrv_priv *priv;
    int i, ret;

    // 1. 分配 net_device
    ndev = alloc_etherdev(sizeof(struct mydrv_priv));
    if (!ndev)
        return -ENOMEM;

    pci_set_drvdata(pdev, ndev);
    priv = netdev_priv(ndev);

    // 2. 初始化硬件(省略具体实现)
    ret = mydrv_init_hw(pdev, priv);
    if (ret)
        goto err_free_netdev;

    // 3. 为每个 RX 队列注册 NAPI
    for (i = 0; i < priv->num_rx_queues; i++) {
        netif_napi_add(ndev, &priv->napi[i], mydrv_poll, 64);
    }

    // 4. 注册网络设备
    ret = register_netdev(ndev);
    if (ret)
        goto err_free_napi;

    return 0;

err_free_napi:
    // 清理已注册的 NAPI 实例
    for (i = 0; i < priv->num_rx_queues; i++)
        netif_napi_del(&priv->napi[i]);
err_free_netdev:
    free_netdev(ndev);
    return ret;
}
常见错误: 部分驱动在 ndo_open 中才调用 netif_napi_add(),这会导致设备注册后、接口 up 前,NAPI 实例尚未就绪。若此时内核触发链路事件或统计查询,可能访问未初始化的 NAPI 链表,引发空指针解引用。务必在 register_netdev() 之前完成 NAPI 注册。
probe 阶段 NAPI 注册时序 驱动 probe 阶段 NAPI 注册时序 alloc_etherdev 硬件初始化 netif_napi_add register_netdev 关键原则 NAPI 注册必须在 register_netdev() 之前完成 确保设备对外可见时,NAPI 实例已完全就绪 避免在 ndo_open 中延迟注册

3.3 多队列网卡的 NAPI 注册策略

现代高速网卡普遍支持多 RX 队列(RSS / 多队列),每个队列可以独立触发中断并拥有独立的 NAPI 实例。多队列场景下的 NAPI 注册策略直接影响 CPU 负载均衡和收包性能。

3.3.1 一对一策略:每队列独立 NAPI

这是最常用的策略,每个 RX 硬件队列对应一个独立的 napi_struct 实例。中断亲和性(IRQ affinity)将每个队列的中断绑定到不同的 CPU 核心,实现并行收包。

// 一对一注册示例
for (i = 0; i < priv->num_rx_queues; i++) {
    netif_napi_add(ndev, &priv->napi[i], mydrv_poll, 64);
    // 后续通过 irq_set_affinity_hint 将队列 i 的中断绑定到 CPU i
}

3.3.2 多对一策略:多队列共享 NAPI

在低端硬件或虚拟化场景中,多个 RX 队列可以共享同一个 NAPI 实例。此时 poll 回调需要遍历所有共享队列,从每个队列中收取报文。该策略减少了 NAPI 实例数量,但牺牲了并行度。

// 多对一注册示例(所有队列共享一个 NAPI)
netif_napi_add(ndev, &priv->napi, mydrv_poll_multi, 64);

// poll 回调中遍历所有队列
static int mydrv_poll_multi(struct napi_struct *napi, int budget)
{
    struct mydrv_priv *priv = container_of(napi, struct mydrv_priv, napi);
    int work_done = 0;
    int i;

    for (i = 0; i < priv->num_rx_queues; i++) {
        work_done += mydrv_poll_queue(&priv->rx_queue[i], budget - work_done);
        if (work_done >= budget)
            break;
    }
    return work_done;
}

3.3.3 策略对比

策略 NAPI 实例数 并行度 适用场景 中断亲和性要求
一对一(每队列独立 NAPI) 等于 RX 队列数 高,每个队列可运行在不同 CPU 高性能物理网卡、多核服务器 必须为每个队列设置独立 CPU 亲和性
多对一(共享 NAPI) 1 低,所有队列串行处理 嵌入式设备、虚拟网卡、队列数少的场景 不需要,所有中断共享同一 CPU
分组共享(N 对 M) 介于 1 和队列数之间 中等 需要平衡性能和资源消耗的场景 每组绑定一个 CPU
性能建议: 对于 10Gbps 及以上网卡,强烈推荐使用一对一策略。每个 NAPI 实例绑定独立 CPU 后,可以充分利用多核能力,避免单 CPU 成为收包瓶颈。同时注意将 weight 值设置为 64,配合 gro 和 tso 卸载特性,可获得最佳吞吐表现。
多队列 NAPI 注册策略对比 多队列 NAPI 注册策略对比 一对一策略 RX Queue 0 RX Queue 1 RX Queue 2 NAPI 0 NAPI 1 NAPI 2 CPU 0 CPU 1 CPU 2 多对一策略 RX Queue 0 RX Queue 1 RX Queue 2 NAPI 共享 CPU 0

3.4 注册后的清理与反注册

在驱动卸载(remove 回调)或设备热拔时,必须使用 netif_napi_del() 反注册所有 NAPI 实例。该函数将 NAPI 实例从设备的 napi_list 中移除,并确保没有正在执行的 poll 回调。

static void mydrv_remove(struct pci_dev *pdev)
{
    struct net_device *ndev = pci_get_drvdata(pdev);
    struct mydrv_priv *priv = netdev_priv(ndev);
    int i;

    unregister_netdev(ndev);

    // 反注册所有 NAPI 实例
    for (i = 0; i < priv->num_rx_queues; i++)
        netif_napi_del(&priv->napi[i]);

    free_netdev(ndev);
}
注意: netif_napi_del() 必须在 unregister_netdev() 之后调用,以确保设备已从内核网络子系统中移除,不会有新的中断或软中断触发该 NAPI 实例。同时,调用 netif_napi_del() 前应确保所有中断已被禁用,且 NAPI 实例已处于停止状态(通过 napi_disable() 实现)。