3. NAPI 核心设计哲学:轮询(Polling)与中断结合

3.1 NAPI 的诞生背景:中断风暴与系统崩溃

在 NAPI 诞生之前(Linux 2.4 及更早版本),内核网络收包完全依赖传统中断驱动模型。每当网卡收到一个数据包,就会触发一次硬件中断(IRQ),CPU 立即暂停当前任务,进入中断上下文执行收包操作。这种设计在低速网络(10/100 Mbps)下尚可工作,但进入千兆(Gigabit)时代后暴露了致命缺陷:

中断风暴(Interrupt Storm):当网络流量激增时,每个数据包都产生一次中断。CPU 被频繁打断,大量时间消耗在中断上下文切换和寄存器保存/恢复上,而非实际的数据包处理。实测表明,在 1 Gbps 线速下,传统中断模型会导致 CPU 占用率接近 100%,而有效吞吐量却急剧下降。

传统中断模型的瓶颈可归纳为以下三点:

问题 现象 后果
中断频率过高 每个数据包触发一次 IRQ CPU 被中断淹没,无法处理上层协议
缓存污染严重 中断上下文频繁切换,L1/L2 缓存不断刷新 内存访问延迟增加,整体性能下降
不可扩展性 多队列网卡(RSS)下,每个队列独立中断 多核 CPU 负载不均,甚至触发活锁(Livelock)
活锁(Livelock):这是传统中断模型最严重的灾难。当网络流量超过 CPU 的处理能力时,CPU 永远在处理中断和收包,用户态进程完全得不到 CPU 时间,系统看似"活着"但实际已停止响应。NAPI 的核心目标之一就是彻底消除活锁。

3.2 NAPI 的设计目标:中断与轮询的优雅结合

NAPI(New API)由 David S. Miller 等人于 Linux 2.5 开发,并在 Linux 2.6 中正式合入主线。其设计哲学可概括为:"中断唤醒,轮询收包"。核心思想是:用中断通知 CPU 有数据到达,但后续的数据包不再触发中断,而是由 CPU 主动轮询(Polling)网卡接收队列,直到队列为空。

NAPI 的三大设计目标:

  1. 消除活锁:通过轮询模式限制 CPU 在收包上的时间片,确保用户态进程不会被饿死。
  2. 降低中断开销:将 N 个数据包的中断次数从 N 次降低到 1 次,大幅减少上下文切换。
  3. 提高缓存局部性:轮询模式下,CPU 连续处理多个数据包,数据结构和描述符更可能驻留在缓存中。
NAPI 核心设计哲学:中断唤醒 + 轮询收包 传统中断模型(NAPI 前) 网卡(NIC) 每个包一次 IRQ CPU 中断 ⚠ 中断风暴 ⚠ 缓存污染 ⚠ 活锁风险 NAPI 模型(中断 + 轮询) 网卡(NIC) 第一个包 IRQ CPU 轮询 后续包:无中断,直接轮询 收包队列 批量读取 协议栈 ✓ 中断次数:N → 1 ✓ 无活锁 ✓ 缓存友好

3.3 NAPI 的工作流程:状态机视角

NAPI 的核心是一个简单的状态机,每个网卡设备(或每个 RX 队列)维护一个 napi_struct 结构体,其状态在 中断使能 和 轮询中 之间切换:

关键数据结构:struct napi_struct 包含 poll_list(挂入全局轮询链表)、state(NAPI_STATE_SCHED 标志位)、poll(轮询回调函数指针)和 weight(单次轮询最大处理包数)。
  1. 初始状态:网卡中断使能,NAPI 处于空闲状态。
  2. 中断到达:网卡收到第一个数据包,触发硬件中断。中断处理函数调用 napi_schedule(),将设备的 napi_struct 加入 CPU 的 softnet_data.poll_list,并设置 NAPI_STATE_SCHED 标志。
  3. 关闭中断:NAPI 要求驱动在中断处理函数中关闭该设备的硬件中断,后续数据包不再产生 IRQ。
  4. 触发软中断:中断处理函数返回前,触发 NET_RX_SOFTIRQ 软中断。
  5. 轮询阶段:软中断处理函数 net_rx_action() 遍历 poll_list,调用每个设备的 poll 回调。驱动在 poll 中从网卡接收描述符批量读取数据包,送入协议栈。
  6. 退出条件:当以下任一条件满足时,轮询结束:
    • 接收队列为空(所有包已处理完毕)。
    • 本次轮询处理的包数达到 weight 上限(默认 64)。
    • 轮询时间片耗尽(防止 CPU 被独占)。
  7. 重新开启中断:如果队列为空,驱动调用 napi_complete() 清除 NAPI_STATE_SCHED 标志,并重新开启硬件中断,回到初始状态。
NAPI 状态机:中断唤醒 → 轮询收包 → 中断重开 中断使能 等待数据包 中断到达 napi_schedule() 轮询中 poll() 批量收包 中断关闭 队列空 / 达到 weight napi_complete() 数据包到达 关闭中断,触发软中断 队列空 或 达到 weight 重新开启中断

3.4 NAPI 与传统中断模型的对比

对比维度 传统中断模型 NAPI 模型
中断频率 每个数据包一次中断 每轮收包一次中断(批量处理)
活锁风险 高(流量超过 CPU 能力时触发) 无(轮询有 weight 上限和 time budget)
缓存效率 低(频繁中断导致缓存抖动) 高(连续轮询,数据局部性好)
适用场景 低流量、延迟敏感型 高吞吐、批量收包场景
CPU 开销 中断上下文切换开销大 轮询上下文开销小,但 CPU 占用持续
实现复杂度 驱动实现简单 驱动需实现 poll 回调,状态管理复杂
性能调优启示:NAPI 并非银弹。在低流量场景(如 IoT、控制面报文),NAPI 的轮询机制可能导致不必要的 CPU 空转。此时可以通过调整 weight 参数或使用 gro_flush_timeout 来控制轮询行为,实现"低延迟 vs 高吞吐"的平衡。这部分内容将在后续章节详细展开。

3.5 NAPI 的扩展:从单队列到多队列

随着多核 CPU 和多队列网卡(RSS、RPS、RFS)的普及,NAPI 也演进为每队列独立 NAPI 实例的模式。每个 RX 队列拥有独立的 napi_struct,可以绑定到不同的 CPU 核心,实现并行收包:

  • RSS(Receive Side Scaling):硬件层面将数据流分发到多个 RX 队列,每个队列对应一个 NAPI 实例,由不同 CPU 处理。
  • RPS(Receive Packet Steering):软件层面模拟 RSS,将数据包分发到不同 CPU 的 backlog 队列。
  • RFS(Receive Flow Steering):在 RPS 基础上,根据流量的应用层 CPU 亲和性进行分发,提升缓存命中率。

多队列 NAPI 的引入,使得内核网络收包从"单核瓶颈"进化为"多核并行",但同时也带来了新的调优挑战:队列数、中断亲和性、轮询权重三者需要协同配置,才能发挥硬件的最佳性能。

关键调优参数预览:net.core.netdev_budget(全局轮询预算)、net.core.netdev_budget_usecs(轮询时间预算)、/sys/class/net/eth0/queues/rx-*/napi_defer_hard_irqs(延迟中断合并)等,将在后续章节逐一深入。