2. NAPI 数据结构:struct napi_struct 结构体详解、poll 函数指针、状态标志位

NAPI 的核心数据结构是 struct napi_struct,它串联了驱动、中断处理、轮询回调与内核网络子系统。理解该结构体的每个字段、poll 函数指针的契约以及状态标志位的切换逻辑,是后续分析收包路径与性能优化的基础。

2.1 struct napi_struct 结构体全景

该结构体定义在 <linux/netdevice.h> 中,是每个 NAPI 实例的“控制块”。下面列出核心字段及其作用:

字段 类型 作用描述
poll_list struct list_head 将 NAPI 实例链接到全局 napi_hash 或 CPU 的 softnet_data 轮询链表上。
poll int (*poll)(struct napi_struct *, int) 驱动注册的轮询回调函数,NAPI 核心通过此指针从驱动批量收取数据包。
weight int 单次 poll 调用允许处理的最大数据包数量(NAPI 预算)。典型值为 64。
state unsigned long 位图标志,记录 NAPI 实例的调度、禁用、挂起等状态。
gro_list struct list_head GRO(Generic Receive Offload)同流数据包链表,用于合并小包。
skb struct sk_buff * 指向当前正在构建的 GRO 合并 skb,或用于其他临时用途。
dev struct net_device * 关联的网络设备,标识该 NAPI 实例属于哪个网卡。
napi_id int 用于 RPS(Receive Packet Steering)哈希的 ID,帮助将中断分布到多个 CPU。
irq unsigned int 绑定的中断号,用于调试和 /proc/interrupts 统计。
关键设计思想: struct napi_struct 将中断上下文与轮询上下文解耦。驱动在中断中仅触发 NAPI 调度,实际收包工作全部在 poll 回调中完成,从而避免中断处理占用过多 CPU 时间。

2.2 poll 函数指针:契约与实现模式

poll 是 NAPI 驱动必须实现的回调函数,其原型为:

int poll(struct napi_struct *napi, int budget);

该函数遵循严格的契约:

  • 返回值:返回实际处理的包数量(小于等于 budget)。
  • budget 语义:驱动单次 poll 最多处理 budget 个数据包。典型值为 napi->weight(通常 64)。
  • 退出条件:
    • 如果驱动处理完所有待收包且返回包数 < budget,NAPI 核心会关闭轮询模式,重新启用硬件中断。
    • 如果返回包数 == budget,表示仍有包待处理,NAPI 核心将保持轮询状态,并在后续软中断中再次调用 poll。
  • 不可阻塞:poll 在软中断上下文中执行,不能睡眠或调用可能引起调度的函数。
典型驱动实现模板:
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;

    while (work_done < budget) {
        struct sk_buff *skb = my_hw_receive(priv);
        if (!skb)
            break;
        napi_gro_receive(napi, skb);  // 送入 GRO 或协议栈
        work_done++;
    }

    if (work_done < budget) {
        // 没有更多包,关闭轮询,重新开启中断
        napi_complete_done(napi, work_done);
        my_enable_hw_interrupt(priv);
    }

    return work_done;
}

2.3 状态标志位:NAPI_STATE_SCHED 及其他

state 字段是一个 unsigned long 位图,每个位代表一种状态。内核通过 test_bit、set_bit、clear_bit 原子操作来修改这些标志,确保多核安全。

标志位 位值 含义
NAPI_STATE_SCHED 0 NAPI 实例已被调度到轮询队列中,正在或即将被 poll 处理。这是最核心的标志位。
NAPI_STATE_DISABLE 1 NAPI 被显式禁用(如设备 down 时),阻止任何新的调度。
NAPI_STATE_NPSVC 2 用于 Netpoll 场景,表示 NAPI 实例被 Netpoll 服务占用。
NAPI_STATE_LISTED 3 NAPI 实例已加入全局 napi_hash 链表,可被查找。
NAPI_STATE_NO_BUSY_POLL 4 禁止对该 NAPI 实例进行忙轮询(busy polling)。
NAPI_STATE_PREFER_BUSY_POLL 5 优先使用忙轮询而非中断驱动,用于低延迟场景。
NAPI_STATE_THREADED 6 NAPI 使用内核线程(kthread)模式执行 poll,而非软中断。
性能关键点: NAPI_STATE_SCHED 的原子测试与设置是 NAPI 调度路径上的热点。内核使用 napi_schedule_prep 函数检查并设置该位,若设置失败(表示已在调度中),则跳过重复调度,避免中断风暴。

2.4 NAPI 状态机与调度流程

下图展示了 NAPI 实例在中断触发、调度、轮询、完成之间的状态转换:

NAPI 状态机与调度流程 NAPI 状态机与调度流程 硬件中断到达 驱动 ISR 触发 napi_schedule() 设置 NAPI_STATE_SCHED 软中断 NET_RX_SOFTIRQ 调用 napi_poll() 驱动 poll() 执行 批量收包 + napi_gro_receive work_done < budget? 判断是否还有包待处理 是(继续轮询) napi_complete() 清除 SCHED + 开中断 核心原则:中断只负责调度,poll 负责实际收包;返回包数 < budget 时关闭轮询并重新使能中断

2.5 关键 API 与原子操作

NAPI 子系统提供一组内联函数来操作 struct napi_struct 的状态,这些函数在驱动和内核网络栈中高频使用:

API 函数 作用 原子操作
napi_schedule_prep 检查并设置 NAPI_STATE_SCHED,若已设置则返回 false test_and_set_bit
__napi_schedule 将 NAPI 实例加入当前 CPU 的 softnet_data->poll_list,并触发软中断 链表操作 + __raise_softirq_irqoff
napi_schedule 组合 napi_schedule_prep + __napi_schedule 复合
napi_complete_done 清除 NAPI_STATE_SCHED,并返回是否关闭轮询 clear_bit
napi_disable 设置 NAPI_STATE_DISABLE,并等待当前 poll 完成 set_bit + 忙等待
napi_enable 清除 NAPI_STATE_DISABLE,允许重新调度 clear_bit
调度路径热点代码:
static inline bool napi_schedule_prep(struct napi_struct *n) {
    return !test_and_set_bit(NAPI_STATE_SCHED, &n->state);
}

static inline void napi_schedule(struct napi_struct *n) {
    if (napi_schedule_prep(n))
        __napi_schedule(n);
}

这段代码是 NAPI 收包路径上最频繁执行的逻辑之一。每次硬件中断都会调用 napi_schedule,但只有第一次会成功设置 NAPI_STATE_SCHED,后续中断直接返回,从而避免中断风暴。

2.6 多队列网卡与多个 NAPI 实例

现代多队列网卡(如 Intel ixgbe、Mellanox mlx5)通常为每个 RX 队列分配独立的 struct napi_struct 实例。每个队列绑定到特定 CPU,实现收包负载均衡:

多队列网卡 NAPI 实例分布 多队列网卡 NAPI 实例与 CPU 绑定 网卡硬件 RX Queue 0 RX Queue 1 RX Queue 2 napi_struct 0 napi_struct 1 napi_struct 2 CPU 0 CPU 1 CPU 2 每个 RX 队列绑定独立的 NAPI 实例,并固定到特定 CPU,实现收包无锁并行

这种 1:1:1 的映射关系(RX 队列 → NAPI 实例 → CPU)是现代高性能网卡的标准配置,避免了多 CPU 竞争同一个 NAPI 实例的 state 位和 poll_list 链表。

性能优化启示: 在调优时,可以通过 /proc/irq/<irq_num>/smp_affinity 调整中断亲和性,确保 NAPI 实例的 poll 回调始终在目标 CPU 上执行。同时,napi_struct 的 weight 值可通过 /sys/class/net/<iface>/gro_flush_timeout 等参数间接调整,影响单次轮询的包处理量。