4. NAPI 数据结构:napi_struct 结构体详解、poll 函数指针、设备与 NAPI 的绑定关系

NAPI 机制的核心数据结构是 napi_struct,它连接了网络设备、中断处理与轮询收包逻辑。理解该结构体的字段含义、poll 函数指针的注册机制以及设备与 NAPI 实例的绑定关系,是进行中断合并参数调优的基础。

4.1 napi_struct 结构体核心字段

napi_struct 定义在 <linux/netdevice.h> 中,每个 NAPI 实例代表一个可被轮询的收包队列。其关键字段如下表所示:

字段名 类型 作用描述
poll_list struct list_head 用于将 NAPI 实例链接到全局的 softnet_data->poll_list 链表中,供软中断遍历调度。
state unsigned long 标志位字段,包含 NAPI_STATE_SCHED(已调度)、NAPI_STATE_DISABLE(禁用)等状态。
weight int 单次轮询预算,即一次 poll 调用最多处理的报文数。典型值为 64。
poll poll_func * 函数指针,指向设备驱动注册的轮询回调函数。这是 NAPI 收包的核心入口。
dev struct net_device * 指向所属的网络设备,用于关联设备上下文和统计信息。
skb struct sk_buff * 用于 GRO(Generic Receive Offload)的临时 skb 指针,辅助合并小包。
napi_id int NAPI 实例的唯一 ID,用于 RPS(Receive Packet Steering)和跟踪。
irq unsigned int 绑定的中断号,用于中断上下文与 NAPI 的关联。
关键理解: weight 和 poll 是调优的核心。weight 决定了每次轮询的“工作量”,而 poll 函数指针决定了实际收包逻辑。中断合并参数(如 rx-usecs)通过影响 NAPI 的调度频率间接影响吞吐与延迟。

4.2 poll 函数指针:驱动注册的收包回调

poll 函数指针是 NAPI 机制的灵魂。驱动在初始化 NAPI 实例时,必须注册一个符合 poll_func 签名的回调函数。该函数在软中断上下文(ksoftirqd)中被调用,负责从硬件队列中批量收取报文并上送协议栈。

函数签名:

int (*poll)(struct napi_struct *napi, int budget);
  • napi:指向当前 NAPI 实例的指针,驱动可通过 napi->dev 获取设备信息。
  • budget:本次轮询允许处理的最大报文数,由软中断调度时传入,通常等于 napi->weight。
  • 返回值:实际处理的报文数量。若返回值等于 budget,表示仍有剩余报文待处理,NAPI 保持调度状态;若小于 budget,表示队列已空,驱动应调用 napi_complete() 退出轮询并重新开启中断。

典型驱动实现流程:

  1. 从硬件描述符环中读取已接收的报文。
  2. 调用 napi_gro_receive() 或 netif_receive_skb() 将 skb 送入协议栈。
  3. 统计处理数量,若达到 budget 则返回 budget;否则返回实际处理数。
  4. 若返回小于 budget,调用 napi_complete() 并重新使能硬件中断。
调优提示: 高性能驱动(如 mlx5、i40e)的 poll 函数通常包含硬件预取、批量 DMA 回收和 GRO 合并逻辑。调整 weight 可以控制单次轮询的 CPU 时间片占用,避免长时间占用软中断导致其他任务饥饿。

4.3 设备与 NAPI 的绑定关系

网络设备与 NAPI 实例的绑定通过 netif_napi_add() 函数在驱动初始化时建立。绑定关系决定了中断触发后,哪个 NAPI 实例被调度。

绑定流程:

  • 驱动在探测(probe)阶段为每个 RX 队列或队列组分配一个 napi_struct 实例。
  • 调用 netif_napi_add(dev, &napi, poll_func, weight) 将 NAPI 实例注册到设备 dev 上。
  • 设备的中断处理函数(ISR)中,调用 napi_schedule(&napi) 将 NAPI 实例加入当前 CPU 的 poll_list,并触发软中断。

多队列场景下的绑定关系:

场景 绑定方式 典型配置
单队列设备 一个设备绑定一个 NAPI 实例 eth0 → napi0
多队列(RSS) 每个 RX 队列绑定一个独立的 NAPI 实例 eth0-queue0 → napi0, eth0-queue1 → napi1
多队列(XPS/RPS) NAPI 实例与 CPU 亲和性绑定 napi0 绑定 CPU0, napi1 绑定 CPU1
注意事项: 绑定关系一旦建立,中断和 NAPI 的调度就固定在了特定 CPU 上。若 CPU 亲和性配置不当,可能导致 CPU 负载不均,影响整体收包性能。使用 irqbalance 或手动设置 /proc/irq/<irq>/smp_affinity 可以优化绑定。

4.4 NAPI 数据结构与调度关系图

下图展示了 napi_struct 与网络设备、中断处理、软中断调度之间的核心关系:

NAPI 数据结构与调度关系图 网络设备 (net_device) eth0 / 多队列网卡 硬件中断 (ISR) napi_struct 实例 state: NAPI_STATE_SCHED weight: 64 poll: 驱动注册的回调 dev: 指向 net_device poll 函数指针 int poll(budget) 软中断 (ksoftirqd) 遍历 poll_list 调度 netif_napi_add() 触发 napi_schedule() 调用 加入 poll_list 调度执行 说明 中断触发 → napi_schedule → 软中断轮询 poll 回调

4.5 多队列绑定与中断合并的协同

在支持 RSS(Receive Side Scaling)的多队列网卡中,每个 RX 队列拥有独立的 NAPI 实例和中断。中断合并参数(如 ethtool -C eth0 rx-usecs 100)控制每个队列的中断产生频率,而 NAPI 的 weight 控制单次轮询的收包量。两者协同工作:

  • 中断合并(coalescing):延迟中断触发,积累更多报文后一次性通知 CPU,减少中断次数。
  • NAPI 轮询:中断触发后,NAPI 在软中断中批量收包,直到队列为空或达到 weight 上限。
  • 调优平衡:增大 rx-usecs 可提升吞吐但增加延迟;增大 weight 可减少软中断调度次数但增加单次 CPU 占用。
实践建议: 对于延迟敏感型应用(如 Redis、高频交易),建议设置 rx-usecs = 0(关闭合并)并降低 weight(如 32);对于吞吐型应用(如文件服务器、视频流),建议设置 rx-usecs = 50-200 并提高 weight(如 128-256)。