2. struct napi_struct 结构体深度解析

NAPI 的核心调度单元由 struct napi_struct 描述。每个注册了 NAPI 的网卡驱动都会分配一个该结构体实例,它串联了中断下半部、轮询回调、设备亲和性以及内核网络栈的收包入口。理解该结构体的每个成员,是优化收包路径的前提。

2.1 结构体全景

以下代码取自 Linux 内核 <linux/netdevice.h>,为便于讲解,省略了部分调试与统计字段,保留核心成员:

struct napi_struct {
    /* 用于将当前 napi 挂入 CPU 的 poll_list 链表 */
    struct list_head    poll_list;

    /* 状态标志位,使用 unsigned long 的位操作 */
    unsigned long       state;

    /* 一次 poll 调用最多处理的报文数 */
    int                 weight;

    /* 轮询回调函数,驱动必须实现 */
    int                 (*poll)(struct napi_struct *napi, int budget);

    /* 所属的网络设备 */
    struct net_device   *dev;

    /* 挂入 gro_list 的已聚合数据包链表 */
    struct list_head    gro_list;

    /* GRO 计数与时间戳,用于 flush 判断 */
    struct sk_buff      *skb;
    u32                 gro_count;

    /* 中断亲和性与调度控制 */
    int                 napi_id;
    struct rcu_head     rcu;

    /* 硬件时间戳与 IRQ 映射 */
    struct hrtimer      timer;
    unsigned int        napi_idr;
};
关键认知:NAPI 结构体本质是一个“软中断上下文中的轮询控制器”。驱动不需要自己管理收包线程,内核通过 napi_schedule() 将本结构体挂入 CPU 的待调度链表,由软中断统一调度 poll 回调。

2.2 核心成员详解

成员 类型 作用 配置要点
poll_list struct list_head 将本 napi 挂入当前 CPU 的 softnet_data.poll_list 链表 由 napi_schedule() 自动操作,驱动不应直接修改
state unsigned long 位图标志,控制 NAPI 的调度状态(禁用、调度中、已挂起等) 通过 napi_disable()/napi_enable() 修改
weight int 单次 poll 调用的最大收包预算 通常为 64,GRO 场景可适当增大
poll 函数指针 驱动实现的收包回调,内核软中断中调用 必须实现,返回实际处理的报文数
dev struct net_device * 关联的网卡设备 在 netif_napi_add() 时绑定
gro_list struct list_head GRO 同流数据包链表 由 napi_gro_receive() 维护

2.3 state 位图:NAPI 的“运行状态寄存器”

state 字段以位操作管理 NAPI 生命周期,驱动与内核通过预定义掩码判断状态:

/* 内核预定义的状态位 */
enum napi_state {
    NAPI_STATE_SCHED,      /* Bit 0: 已被调度,等待 poll */
    NAPI_STATE_MISSED,     /* Bit 1: 调度期间又有新中断到达 */
    NAPI_STATE_DISABLE,    /* Bit 2: 被驱动禁用,不可调度 */
    NAPI_STATE_NPSVC,      /* Bit 3: 非优先服务,用于 busy poll */
    NAPI_STATE_LISTED,     /* Bit 4: 已挂入 poll_list */
    NAPI_STATE_NO_BUSY_POLL,/* Bit 5: 禁止 busy poll */
    NAPI_STATE_IN_BUSY_POLL,/* Bit 6: 正在 busy poll */
};
调度流程中的状态变迁:
  1. 中断到来 → napi_schedule() 设置 NAPI_STATE_SCHED,将 napi 挂入 poll_list。
  2. 软中断执行 → 清除 NAPI_STATE_SCHED,调用 poll 回调。
  3. 若 poll 未用完 budget → 清除 NAPI_STATE_SCHED,重新使能中断。
  4. 若 poll 用完 budget → 保留 NAPI_STATE_SCHED,再次挂入链表等待下一轮。
  5. 若 poll 执行期间又有中断 → 设置 NAPI_STATE_MISSED,避免丢失事件。

2.4 weight 与 poll:收包预算与回调契约

weight 决定了单次软中断中 poll 最多能处理的报文数。内核默认值为 64,驱动可在 netif_napi_add() 时指定:

/* 驱动初始化 NAPI 的典型代码 */
netif_napi_add(netdev, &priv->napi, napi_poll, 64);

poll 回调必须遵循以下契约:

  • 返回值:实际处理的报文数(≤ budget)。
  • 退出条件:若处理数 < budget,表示当前无更多报文,内核会清除 NAPI_STATE_SCHED 并重新使能中断。
  • 继续条件:若处理数 == budget,表示仍有报文待收,内核保留 NAPI_STATE_SCHED,将 napi 重新加入 poll_list 等待下一轮调度。
  • 禁止睡眠:poll 在软中断上下文中执行,不能调用可能导致睡眠的函数。
常见误区:weight 不是越大越好。过大的 weight 会导致单次软中断耗时过长,增加网络延迟抖动。对于万兆网卡,建议 64~128;对于 25G/40G 网卡,可结合 GRO 适当提升至 256,但需实测 CPU 开销。

2.5 结构体关系与调度流程

下图展示了 napi_struct 与 CPU 软中断、设备驱动之间的核心关系:

struct napi_struct 调度关系图 硬件中断到达 驱动 ISR napi_schedule() struct napi_struct 实例 poll_list → CPU链表 state (SCHED/MISSED) weight = 64 poll = napi_poll() CPU 软中断 (NET_RX) 遍历 poll_list 调用 poll 回调 说明:napi_struct 是中断与软中断之间的桥梁,通过 poll_list 实现跨 CPU 的负载均衡

2.6 配置建议与最佳实践

  • weight 与 budget 的关系:内核软中断的全局 budget 由 net.core.netdev_budget 控制(默认 300),每个 napi 的 weight 不应超过该值。
  • 多队列场景:每个 RX 队列应独立分配一个 napi_struct,通过 netif_napi_add() 注册,并利用 irq_set_affinity_hint() 将中断绑定到不同 CPU。
  • 动态调整:通过 /sys/class/net/<iface>/queues/rx-<n>/napi_defer_hard_irqs 和 gro_flush_timeout 可微调 NAPI 的调度行为,降低高吞吐下的中断频率。
  • 调试接口:/proc/net/softnet_stat 可观察每个 CPU 的 NAPI 调度次数、超限次数和挤压情况。
驱动开发检查清单:
  • ✅ 在 probe 中调用 netif_napi_add() 注册 napi,指定合理的 weight。
  • ✅ 在中断处理函数中调用 napi_schedule() 而非直接收包。
  • ✅ 在 poll 回调中调用 napi_complete() 退出轮询,并重新使能中断。
  • ✅ 在 remove 中调用 netif_napi_del() 清理资源。
  • ✅ 使用 napi_disable()/napi_enable() 控制设备开/关时的 NAPI 状态。