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

NAPI 机制的核心调度单元是 struct napi_struct。每个注册了 NAPI 的网络设备(或虚拟设备队列)都拥有一个该结构体的实例。它负责管理轮询状态、挂载待处理设备、绑定轮询回调以及控制流量反压。理解该结构体的每个字段,是编写高效 NAPI 驱动的基础。

关键认知: napi_struct 是驱动与内核网络子系统之间的“契约”。驱动通过它告诉内核“我有数据要轮询”,内核通过它控制驱动“你该停下来了”。

3.1 结构体全景视图

以下 SVG 展示了 napi_struct 在 NAPI 调度路径中的核心位置及其与上下层模块的关系。

struct napi_struct 核心位置与关联 struct napi_struct 核心位置与关联 网络设备 (net_device) struct napi_struct state (unsigned long) poll_list (struct list_head) poll (poll_fn) weight (int) dev_list (struct list_head) napi_id / irq NET_RX_SOFTIRQ 软中断 驱动 poll() 回调函数 调度入口 硬件中断触发 napi_schedule() 挂入 poll_list 反压机制 weight 耗尽 → NAPI_STATE_PEDING 保留

3.2 核心字段详解

字段名 类型 核心作用 驱动开发者关注点
state unsigned long NAPI 实例的状态标志位,使用位操作管理 检查 NAPI_STATE_SCHED 判断是否已在调度中
poll_list struct list_head 将 NAPI 实例链接到全局或 per-CPU 的轮询链表 由 napi_schedule() 自动管理,驱动通常不直接操作
poll int (*poll)(struct napi_struct *, int budget) 驱动实现的轮询回调函数指针 必须实现,返回处理的数据包数量,不能超过 budget
weight int 单次轮询允许处理的最大数据包预算 通常设置为队列长度或 64,过大会导致软中断饥饿
dev_list struct list_head 将 NAPI 实例挂入所属设备的 NAPI 列表 设备初始化时通过 netif_napi_add() 自动关联
napi_id int 用于跟踪和调试的唯一 ID 内核内部使用,驱动通常只读

3.3 state 字段:状态机与位标志

state 是 NAPI 实例的“大脑”,通过位操作控制调度生命周期。内核定义了以下关键状态位:

  • NAPI_STATE_SCHED (bit 0):核心标志。置位表示 NAPI 实例已被调度,正在或即将被轮询。清除表示轮询结束,可以重新触发调度。
  • NAPI_STATE_MISSED (bit 1):在轮询期间又有新中断到达,但无法再次调度(因为 SCHED 已置位)。内核会在轮询结束后自动重新触发调度。
  • NAPI_STATE_DISABLE (bit 2):表示 NAPI 被显式禁用(如设备 down)。此时 napi_schedule() 不会生效。
  • NAPI_STATE_NPSVC (bit 3):用于 netpoll 场景,防止递归轮询。
  • NAPI_STATE_LISTED (bit 4):表示该 NAPI 实例已加入设备的 NAPI 列表。
驱动开发技巧: 在 poll() 回调中,不要手动修改 state。使用 napi_complete_done() 来安全地清除 NAPI_STATE_SCHED 并重新启用中断。直接调用 clear_bit() 可能导致竞态条件。

3.4 poll_list:调度链表的挂载与遍历

poll_list 是 NAPI 调度系统的“传送带”。当驱动调用 napi_schedule() 时,内核将该 NAPI 实例挂入当前 CPU 的 softnet_data->poll_list。软中断处理函数 net_rx_action() 遍历该链表,依次调用每个 NAPI 实例的 poll() 回调。

以下 SVG 展示了 poll_list 在调度过程中的动态变化:

poll_list 调度链表动态流程 poll_list 调度链表动态流程 阶段 1:空闲 poll_list 为空 中断使能,等待数据 state = 0 中断到达 阶段 2:调度 napi_schedule() 调用 挂入 poll_list state |= NAPI_STATE_SCHED 软中断触发 阶段 3:轮询 net_rx_action() 调用 poll() 回调 处理 sk_buff 轮询结束决策 处理完所有数据 weight 耗尽 napi_complete_done() 清除 SCHED,重新使能中断 保留 SCHED,继续轮询 下次软中断继续处理

3.5 poll 与 weight:轮询契约

poll 是驱动开发者必须实现的函数指针,weight 是内核给予驱动的“单次预算”。两者共同构成了 NAPI 的流量反压契约:

  • budget 参数:内核传入的 budget 是全局预算(通常为 300),而 napi->weight 是每个 NAPI 实例的本地预算。poll() 返回实际处理的数据包数量,不能超过 weight。
  • 返回值语义:返回 weight 表示“我还有数据要处理,请继续调度我”;返回小于 weight 的值表示“我已经处理完了,可以休息了”。
  • 反压触发:如果所有 NAPI 实例都返回 weight,内核会认为 CPU 过载,触发反压机制(如暂停该 CPU 的轮询)。
常见错误: 在 poll() 中返回 0 但实际还有数据未处理,会导致数据包丢失。返回大于 weight 的值会被内核截断,并可能触发调度异常。务必保证返回值与实际处理数量一致。

3.6 驱动初始化与注册流程

以下代码片段展示了驱动如何初始化 napi_struct 并注册到内核:

#include <linux/netdevice.h>
#include <linux/interrupt.h>

/* 驱动私有数据结构 */
struct my_priv {
    struct napi_struct napi;
    struct net_device *ndev;
    /* ... 其他字段 ... */
};

/* 轮询回调实现 */
static int my_poll(struct napi_struct *napi, int budget)
{
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;

    /* 从硬件接收队列批量获取 sk_buff */
    while (work_done < budget) {
        struct sk_buff *skb = my_hw_dequeue(priv);
        if (!skb)
            break;
        napi_gro_receive(napi, skb);  /* 或 netif_receive_skb */
        work_done++;
    }

    /* 判断是否处理完毕 */
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        /* 重新使能硬件中断 */
        my_enable_irq(priv);
    }

    return work_done;
}

/* 设备打开回调 */
static int my_open(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 初始化 NAPI:关联 poll 回调,设置 weight */
    netif_napi_add(ndev, &priv->napi, my_poll, 64);

    /* 使能 NAPI */
    napi_enable(&priv->napi);

    /* 使能硬件中断 */
    my_enable_irq(priv);
    return 0;
}

/* 中断处理函数(上半部) */
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
    struct my_priv *priv = dev_id;

    /* 禁用中断,防止风暴 */
    my_disable_irq(priv);

    /* 调度 NAPI 轮询 */
    napi_schedule(&priv->napi);

    return IRQ_HANDLED;
}

3.7 关键 API 速查表

API 函数 作用 调用时机
netif_napi_add() 初始化 napi_struct 并关联设备、回调、weight 驱动 probe 或 open 阶段
napi_enable() 清除 NAPI_STATE_DISABLE,使能调度 设备 up 时
napi_disable() 设置 NAPI_STATE_DISABLE,等待当前轮询结束 设备 down 时
napi_schedule() 触发 NAPI 调度(若未在调度中) 中断上半部
napi_complete_done() 标记轮询完成,清除 SCHED 标志 poll() 回调末尾
napi_gro_receive() 将 sk_buff 送入 GRO 合并引擎 poll() 回调中
性能要点: weight 的选择直接影响吞吐量和延迟的平衡。对于高速网卡(10Gbps+),建议 weight 设置为 64~128;对于低速或延迟敏感场景,可降低至 32。过大的 weight 会导致单次轮询占用 CPU 过久,增加其他网络流的延迟。

3.8 本章小结

  • struct napi_struct 是 NAPI 调度的核心数据结构,通过 state 管理生命周期,通过 poll_list 参与调度链表。
  • poll 回调是驱动与内核的契约接口,必须严格遵循 budget 和返回值规则。
  • weight 是单次轮询的预算上限,直接影响 CPU 占用和反压行为。
  • 驱动开发者应使用 netif_napi_add() / napi_enable() / napi_schedule() / napi_complete_done() 标准 API,避免直接操作位标志。