3. NAPI 数据结构:napi_struct 结构体详解

NAPI 轮询机制的核心数据结构是 napi_struct,它位于 <linux/netdevice.h> 中。每个支持 NAPI 的网卡驱动都会为每个接收队列(或每个设备)分配一个 napi_struct 实例。该结构体将设备驱动、软中断处理以及轮询逻辑紧密关联在一起,是理解 NAPI 收包流程的基石。

3.1 napi_struct 核心字段

下表列出了 napi_struct 中最关键的字段及其作用:

字段名 类型 作用描述
poll_list struct list_head 用于将 NAPI 实例链接到全局的 softnet_data->poll_list 链表中,供软中断遍历调度。
state unsigned long 状态标志位集合,通过位操作管理 NAPI 实例的生命周期(如调度、禁用、挂起等)。
weight int 单次轮询预算(budget),即一次 poll 回调最多处理的报文数量。通常由驱动初始化时设定(如 64)。
poll int (*poll)(struct napi_struct *, int) 函数指针,指向驱动实现的轮询回调函数。软中断通过调用此函数从硬件队列中批量收取报文。
dev struct net_device * 指向所属的网络设备(网卡)实例,用于关联设备上下文。
skb struct sk_buff * 用于在 GRO(Generic Receive Offload)处理中暂存合并中的 sk_buff,非 GRO 场景下通常为 NULL。
list struct list_head 用于将 NAPI 实例加入驱动自身的私有链表(如多队列网卡的 NAPI 列表)。
关键设计思想: napi_struct 将中断下半部处理从“每中断一次处理一次”转变为“每轮询周期处理一批”,通过 weight 和 poll 的组合实现了收包速率与 CPU 开销之间的自适应平衡。

3.2 poll 函数指针详解

poll 是 NAPI 机制中驱动与内核协议栈之间的核心契约。软中断(net_rx_action)在轮询到某个 NAPI 实例时,会调用该函数指针。驱动开发者必须实现此函数,其典型逻辑如下:

  1. 从硬件 RX 描述符环中批量读取报文:通过 DMA 地址获取已接收的数据包。
  2. 构造 sk_buff 并提交给协议栈:调用 napi_gro_receive() 或 netif_receive_skb() 将报文送入上层处理。
  3. 返回实际处理的报文数量:返回值不能超过传入的 budget(即 napi_struct->weight)。
  4. 判断是否退出轮询:如果处理的报文数小于 budget,说明硬件队列已空,驱动应调用 napi_complete() 结束轮询并重新开启硬中断。
// 典型的驱动 poll 函数伪代码
static int my_napi_poll(struct napi_struct *napi, int budget)
{
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;

    // 1. 从硬件 RX 环中批量取包,最多取 budget 个
    while (work_done < budget) {
        struct sk_buff *skb = my_hw_dequeue(priv);
        if (!skb)
            break; // 硬件队列已空

        // 2. 提交 skb 到协议栈(GRO 路径)
        napi_gro_receive(napi, skb);
        work_done++;
    }

    // 3. 判断是否完成本轮轮询
    if (work_done < budget) {
        // 硬件队列已空,关闭 NAPI 调度,重新开启硬中断
        napi_complete_done(napi, work_done);
        my_hw_enable_interrupts(priv);
    }

    return work_done;
}
性能要点: poll 函数应尽量保持轻量,避免在循环内执行高开销操作(如内存分配失败重试)。驱动通常会在 poll 之前通过 prefetch 指令预取下一个描述符的 DMA 数据,以隐藏内存访问延迟。

3.3 状态标志位:NAPI_STATE_SCHED 等

napi_struct->state 字段使用位图来管理 NAPI 实例的多种状态。内核定义了以下标准标志位:

标志位 位值 含义 典型操作
NAPI_STATE_SCHED 0 NAPI 已被调度,当前正在轮询或即将被轮询。这是最核心的标志位。 napi_schedule() 设置;napi_complete() 清除。
NAPI_STATE_MISSED 1 在 NAPI 处于 SCHED 状态期间,又有新中断到达,表示可能有报文被错过。 中断处理函数中若发现 SCHED 已置位,则设置此位。
NAPI_STATE_DISABLE 2 NAPI 被显式禁用,通常用于设备关闭或调优场景。 napi_disable() 设置;napi_enable() 清除。
NAPI_STATE_NPSVC 3 用于 Netpoll 场景,表示 NAPI 被 Netpoll 上下文持有。 Netpoll 模块内部使用。
NAPI_STATE_LISTED 4 NAPI 实例已加入 softnet_data->poll_list 链表。 napi_schedule() 内部设置;napi_complete() 清除。
NAPI_STATE_NO_BUSY_POLL 5 禁止对该 NAPI 实例进行忙轮询(busy polling)。 由 SO_BUSY_POLL 套接字选项控制。
NAPI_STATE_PREFER_BUSY_POLL 6 优先使用忙轮询而非中断驱动。 用于低延迟场景的配置。

状态转换的核心逻辑:

  • 当硬中断触发时,驱动调用 napi_schedule(),该函数会设置 NAPI_STATE_SCHED 标志,并将 NAPI 实例加入当前 CPU 的 poll_list,然后触发软中断 NET_RX_SOFTIRQ。
  • 软中断 net_rx_action 遍历 poll_list,对每个 NAPI 实例调用其 poll 函数。
  • 当 poll 函数返回且实际处理量小于 budget 时,驱动调用 napi_complete(),该函数清除 NAPI_STATE_SCHED 和 NAPI_STATE_LISTED 标志,并将 NAPI 实例从 poll_list 中移除。
  • 如果在 SCHED 状态下又有新中断到达,中断处理函数会设置 NAPI_STATE_MISSED 标志,确保在下次轮询结束时重新调度 NAPI,避免丢包。
竞态条件注意: NAPI_STATE_SCHED 的测试与设置操作必须使用原子位操作(如 test_and_set_bit),以防止在 SMP(对称多处理)环境下多个 CPU 同时调度同一个 NAPI 实例导致的双重入问题。

3.4 NAPI 数据结构关系图

下图展示了 napi_struct 与软中断、设备驱动以及协议栈之间的协作关系:

NAPI 数据结构与调度关系图 硬件中断到达 驱动中断处理函数 napi_schedule() napi_struct state | weight | poll poll_list | dev softnet_data poll_list 链表 net_rx_action 驱动 poll 回调 协议栈 (GRO/IP) 状态标志位流转 中断 → 设置 SCHED napi_schedule → 设置 LISTED napi_complete → 清除 SCHED

3.5 多队列网卡中的 NAPI 实例布局

现代多队列网卡(如 Intel ixgbe、Mellanox mlx5)通常为每个 RX 队列分配独立的 napi_struct 实例。这种设计允许每个 CPU 核心独立处理一个队列的报文,实现收包处理的并行化。驱动在初始化时会为每个队列创建 NAPI 实例,并将其绑定到特定的 CPU(通过 irq_affinity 或 RPS 机制)。

// 多队列驱动中 NAPI 实例的典型初始化模式
struct my_rx_queue {
    struct napi_struct napi;
    // ... 其他队列相关字段
};

static int my_probe(struct pci_dev *pdev, ...)
{
    struct my_priv *priv;
    int i;

    priv = kzalloc(sizeof(*priv), GFP_KERNEL);
    for (i = 0; i < priv->num_rx_queues; i++) {
        struct my_rx_queue *rxq = &priv->rx_queues[i];
        // 初始化 NAPI,绑定 poll 函数,设置 weight
        netif_napi_add(priv->netdev, &rxq->napi, my_poll, 64);
    }
    // ...
}
优化建议: 在多队列场景下,应确保每个 NAPI 实例的 weight 值与硬件 RX 描述符环的大小匹配。如果 weight 设置过大,可能导致单次轮询耗时过长,增加其他队列的调度延迟;设置过小,则可能频繁触发 napi_complete 和重新调度,增加上下文切换开销。