4. NAPI 机制原理:轮询与中断结合、budget 与 weight 参数、NAPI 调度流程

NAPI(New API)是 Linux 内核为解决传统中断驱动网络在高吞吐场景下产生的 中断风暴(Interrupt Storm) 而设计的混合接收机制。其核心思想是:中断驱动唤醒,轮询批量收包。本章深入剖析 NAPI 的轮询与中断结合原理、关键参数 budget 与 weight 的含义,以及完整的调度流程。

4.1 轮询与中断结合的设计动机

传统网络驱动在每次数据包到达时都会触发硬件中断,CPU 立即进入中断上下文处理。当网络流量极大时,频繁的中断会导致 CPU 被中断上下文完全占据,无法执行用户态进程,系统吞吐量急剧下降。NAPI 通过以下方式解决该问题:

  • 中断触发阶段:第一个数据包到达时,硬件仍产生中断,驱动在中断处理函数中关闭该设备的硬件中断,并将设备的 NAPI 结构体加入轮询列表。
  • 轮询收包阶段:内核在软中断上下文(ksoftirqd 或 NET_RX_SOFTIRQ)中调用驱动的 poll 回调函数,批量从硬件接收队列中取出数据包,直到预算耗尽或队列为空。
  • 重新开启中断:当轮询结束且队列为空时,重新开启硬件中断,等待下一批数据包。
关键优势: 高流量时避免中断风暴,低流量时保持低延迟。NAPI 将中断处理从“每个包一次中断”优化为“每批包一次中断+轮询”。
NAPI 中断与轮询结合状态机 中断等待 硬件中断关闭 中断触发 关闭中断,加入轮询列表 轮询收包 调用 poll 批量收包 数据包到达 关闭中断 队列为空,重新开启中断 NAPI 核心循环:中断唤醒 → 关闭中断 → 轮询收包 → 队列空 → 开启中断

4.2 budget 与 weight 参数详解

NAPI 轮询过程受两个关键参数控制:budget 和 weight。它们共同决定了单次轮询的收包上限和 CPU 时间分配。

参数 作用域 默认值 含义
budget 全局(每个 CPU) 300 单次软中断处理中,所有 NAPI 实例总共最多处理的包数。防止单个 CPU 在软中断中耗时过长。
weight 每个 NAPI 实例 64 单个 NAPI 实例在单次 poll 调用中最多处理的包数。通常与驱动接收队列深度相关。
实践建议: 对于万兆网卡,建议将 weight 提升至 128~256,同时适当增大 budget 至 600~1000,以匹配高吞吐场景。但需注意 budget 过大会增加软中断延迟,影响实时性。

4.2.1 参数协同工作示例

假设系统有 2 个 NAPI 实例(对应 2 个 RX 队列),budget=300,weight=64:

  1. 软中断开始,剩余预算 = 300。
  2. 调度 NAPI 实例 1:poll 最多处理 64 个包,实际处理 50 个,剩余预算 = 250。
  3. 调度 NAPI 实例 2:poll 最多处理 64 个包,实际处理 64 个,剩余预算 = 186。
  4. 实例 1 再次被调度:poll 处理 40 个包,剩余预算 = 146。
  5. 实例 2 再次被调度:poll 处理 60 个包,剩余预算 = 86。
  6. 重复直到剩余预算为 0 或所有实例队列为空。
注意: 如果 weight 设置过大,单个 NAPI 实例可能独占预算,导致其他实例(如其他网卡或队列)被饿死。合理配置应保证 weight ≤ budget / NAPI 实例数。
budget 与 weight 分配示意图 全局 budget 池 (默认 300) NAPI 实例 1 weight=64, 已用=50 NAPI 实例 2 weight=64, 已用=64 NAPI 实例 3 weight=64, 已用=40 剩余预算 = 300 - 50 - 64 - 40 = 146,继续轮询直到预算耗尽或队列空

4.3 NAPI 调度流程

NAPI 的完整调度流程涉及硬件中断、软中断、内核线程 ksoftirqd 以及驱动 poll 回调。以下是标准流程:

步骤 上下文 操作
1 硬件中断 网卡收到数据包,触发 IRQ。CPU 执行驱动注册的中断处理函数 irq_handler。
2 硬件中断 驱动调用 napi_schedule,将 NAPI 结构体加入当前 CPU 的 softnet_data->poll_list,并触发 NET_RX_SOFTIRQ 软中断。
3 软中断 net_rx_action 函数执行,遍历 poll_list,依次调用每个 NAPI 实例的 poll 回调。
4 软中断 poll 回调从硬件接收描述符中批量获取数据包,构造 sk_buff,调用 napi_gro_receive 或 netif_receive_skb 送入协议栈。
5 软中断 如果 poll 处理完所有包(队列空),调用 napi_complete_done 从 poll_list 移除,并重新开启硬件中断。
6 软中断 如果 poll 未处理完(预算耗尽),NAPI 实例保留在 poll_list 中,等待下一次软中断或 ksoftirqd 继续调度。
核心数据结构: struct napi_struct 包含 poll_list 链表节点、poll 函数指针、weight、state(标记是否被调度)等字段。每个设备或 RX 队列对应一个 NAPI 实例。
NAPI 调度流程图 ① 硬件中断触发 ② napi_schedule() ③ NET_RX_SOFTIRQ 软中断 ④ 驱动 poll() 批量收包 队列空? ⑤ napi_complete_done() 开启中断,等待下一批 ⑥ 预算耗尽,保留在列表 下次软中断继续

4.4 NAPI 与 ksoftirqd 的协作

当软中断执行时间过长或频率过高时,内核会将软中断处理委托给 ksoftirqd 内核线程。NAPI 与此机制紧密配合:

  • 直接软中断路径:在中断返回时,如果软中断未处理完且执行时间较短,直接在 irq_exit 中处理。
  • ksoftirqd 路径:如果软中断执行时间超过 2 * HZ / 1000(约 2 个 jiffies),或软中断被重新触发,则唤醒 ksoftirqd 线程在进程上下文继续轮询。
  • NAPI 的适应性:NAPI 的 poll 回调在软中断或 ksoftirqd 中执行,对驱动透明。驱动只需实现 poll 函数,无需关心调度上下文。
性能调优点: 通过调整 /proc/sys/net/core/budget 和 /proc/sys/net/core/netdev_budget_usecs(软中断时间预算),可以控制 NAPI 轮询的 CPU 占用。对于纯轮询模式(如 DPDK),可关闭 NAPI 中断,完全使用用户态轮询。

4.5 本章小结

  • NAPI 通过 中断唤醒 + 轮询收包 解决了高吞吐下的中断风暴问题。
  • budget 控制全局收包上限,weight 控制单个 NAPI 实例的单次收包上限,两者需协同配置。
  • NAPI 调度流程从硬件中断开始,经过 napi_schedule、软中断 net_rx_action、驱动 poll,最终通过 napi_complete_done 重新开启中断。
  • 理解 NAPI 原理是配置中断亲和性、优化多队列网卡性能的基础。