NAPI (New API,但现已无特定含义) 是 Linux 内核中用于高效处理网络数据包的一种机制,旨在减少高负载下的中断开销。
其目的是为了解决传统基于中断的包处理在高流量下的性能问题(“中断活锁”)。通过混合中断和轮询模式,在低负载时使用中断保证低延迟,在高负载时切换到轮询保证高吞吐量。
设备通过中断通知有新数据包 -> 内核调度对应的 NAPI 实例 -> 在软中断上下文(或内核线程)中轮询处理多个数据包 -> 处理完毕后再打开中断。
核心数据结构与 API
struct napi_struct:- 核心数据结构,代表一个 NAPI 实例,保存其状态信息。
- 通常与一个中断或一个队列(RX/TX)相关联。
控制 API (初始化和状态管理):
netif_napi_add()/netif_napi_del(): 向系统添加/删除一个 NAPI 实例(通常附加到网络设备上)。napi_enable()/napi_disable(): 启用/禁用 NAPI 实例。禁用状态下实例不会被调度,poll方法不会被调用。注意:API 非幂等,错误调用顺序可能导致死锁或竞态。
数据路径 API (调度与处理):
napi_schedule(): 核心调度函数。通常在设备的中断处理程序中调用,通知内核有数据需要处理,并获取 NAPI 实例的所有权。napi_schedule_irqoff():napi_schedule()的变体,用于已知在中断上下文中调用的情况,可优化中断屏蔽操作。napi_complete_done(): 完成处理函数。当驱动程序的poll方法处理完所有事件后调用此函数,释放实例的所有权。警告:budget为 0 时绝不能调用;若处理恰好用完budget且工作已完成,需谨慎返回budget - 1或等待下次调用。
驱动程序实现要点
poll方法:- 驱动必须实现的回调函数,由内核调度执行实际的数据包处理工作。
- 参数
budget:限制一次poll调用最多可处理的接收(RX)数据包数量(发送 TX 处理无此限制)。若返回值为budget,表示还有工作未完成,内核会再次调度;若小于budget,表示本轮处理已完成。
中断屏蔽策略:
- 调度 NAPI 后应屏蔽设备中断,防止不必要的重复中断,直到
napi_complete_done()被调用后再解除屏蔽。 - 推荐使用
napi_schedule_prep()检查 +__napi_schedule()调用的组合,确保在屏蔽中断后调度,避免竞态条件。
- 调度 NAPI 后应屏蔽设备中断,防止不必要的重复中断,直到
高级特性与配置
实例映射:
- 现代网卡多队列:通常一个 NAPI 实例对应一个中断源和一个队列对(RX+TX)。但映射关系灵活,可一对多或多对一。
软件中断合并 (Coalescing):
可通过
gro_flush_timeout和napi_defer_hard_irqs参数配置 NAPI 在数据包处理完后启动一个定时器,延迟解除中断屏蔽,以合并更多数据包,减少中断次数,提升吞吐量。可通过 sysfs 或 Netlink (netdev-genl) 按每个 NAPI 实例进行配置。GRO(Generic Receive Offload,通用接收卸载)是 Linux 内核中的一种网络数据包处理机制,主要用于在接收数据时将多个小数据包合并成一个大包,再上传给网络协议栈处理,以此减少 CPU 的开销,提升网络吞吐性能
忙轮询 (Busy Polling):
- 允许用户态进程主动轮询设备,在中断触发前检查并处理数据包,以 CPU 资源换取极低延迟。
- 启用方式:套接字选项
SO_BUSY_POLL、系统参数net.core.busy_poll、io_uringAPI 或基于 epoll 的配置。
基于 epoll 的忙轮询:
- 可从
epoll_wait调用中直接触发 NAPI 处理。 - 要求:添加到同一 epoll 上下文的所有文件描述符应具有相同的 NAPI ID(可通过
SO_INCOMING_NAPI_ID获取)。 - 可通过
ioctl(EPIOCSPARAMS) 设置epoll_params结构,精细控制每个 epoll 上下文的忙轮询超时 (busy_poll_usecs)、预算 (busy_poll_budget) 和偏好 (prefer_busy_poll)。
- 可从
IRQ 缓解 (Mitigation) 与挂起 (Suspension):
- IRQ 缓解:使用
SO_PREFER_BUSY_POLL,承诺应用程序会定期忙轮询,驱动可长时间屏蔽中断。由gro_flush_timeout作为安全机制防止停滞。 - IRQ 挂停:更激进的机制。当
epoll_wait成功获取事件时,启动一个irq-suspend-timeout定时器来挂起(屏蔽)IRQ。只要应用程序持续处理,IRQ 就保持挂起状态,避免任何中断干扰。超时或处理空闲后 fallback 到常规模式。需要与gro_flush_timeout等配合使用。
- IRQ 缓解:使用
线程化 NAPI:
- 可配置 NAPI 在独立的内核线程中运行,而不是软中断上下文。可针对每个网络设备或每个 NAPI 实例进行配置(通过 sysfs 或 Netlink)。
- 优点:可能提供更可预测的调度或利于实时性(
PREEMPT_RT)内核。 - 建议:将 NAPI 线程固定到与处理其中断相同的 CPU 上。
用户空间交互
- NAPI ID:每个 NAPI 实例有唯一 ID,用户空间可通过
SO_INCOMING_NAPI_ID套接字选项获取。 - 查询与配置:可通过 Netlink (
netdev-genl家族) 查询设备队列信息(包括 NAPI ID)和配置每个 NAPI 实例的参数(如合并参数、线程化开关)。可使用tools/net/ynl/下的工具(如cli.py)进行操作。
核心思想:中断通知 + 轮询处理,平衡延迟与吞吐。
实现 poll 方法,正确使用 napi_schedule() 和 napi_complete_done(),管理好中断屏蔽。通过软件中断合并、忙轮询、IRQ 缓解/挂起等机制针对低延迟或高吞吐场景进行精细配置。支持多种处理上下文(软中断、线程),映射关系(实例 - 中断 - 队列)灵活。