NAPI¶
NAPI 是 Linux 网络堆栈使用的事件处理机制。NAPI 这个名字不再代表任何特定的含义 [1]。
在基本操作中,设备通过中断通知主机关于新事件。然后,主机调度一个 NAPI 实例来处理这些事件。设备也可以通过 NAPI 轮询事件,而无需先接收中断(忙轮询)。
NAPI 处理通常发生在软件中断上下文中,但可以选择使用单独的内核线程进行 NAPI 处理。
总而言之,NAPI 从驱动程序中抽象了事件(数据包 Rx 和 Tx)处理的上下文和配置。
驱动 API¶
NAPI 最重要的两个元素是 struct napi_struct 和相关的 poll 方法。struct napi_struct 保存 NAPI 实例的状态,而该方法是特定于驱动程序的事件处理程序。该方法通常会释放已传输的 Tx 数据包并处理新接收的数据包。
控制 API¶
netif_napi_add()
和 netif_napi_del()
从系统中添加/删除 NAPI 实例。这些实例附加到作为参数传递的 netdevice(并在 netdevice 取消注册时自动删除)。实例以禁用状态添加。
napi_enable()
和 napi_disable()
管理禁用状态。禁用的 NAPI 无法被调度,并且保证不会调用其 poll 方法。napi_disable()
等待 NAPI 实例的所有权被释放。
控制 API 不是幂等的。控制 API 调用对于数据路径 API 的并发使用是安全的,但不正确的控制 API 调用序列可能会导致崩溃、死锁或竞争条件。例如,连续多次调用 napi_disable()
将会死锁。
数据路径 API¶
napi_schedule()
是调度 NAPI 轮询的基本方法。驱动程序应在其中断处理程序中调用此函数(有关更多信息,请参见调度和 IRQ 屏蔽)。成功调用 napi_schedule()
将获得 NAPI 实例的所有权。
之后,在 NAPI 被调度后,将调用驱动程序的 poll 方法来处理事件/数据包。该方法接受一个 budget
参数 - 驱动程序可以处理任意数量的 Tx 数据包的完成,但只能处理最多 budget
数量的 Rx 数据包。Rx 处理通常更昂贵。
换句话说,对于 Rx 处理,budget
参数限制了驱动程序在单个轮询中可以处理多少个数据包。当 budget
为 0 时,根本不能使用诸如页面池或 XDP 之类的特定于 Rx 的 API。skb Tx 处理应该不受 budget
的影响发生,但是如果参数为 0,则驱动程序不能调用任何 XDP(或页面池)API。
警告
如果核心尝试仅处理 skb Tx 完成且没有 Rx 或 XDP 数据包,则 budget
参数可能为 0。
poll 方法返回已完成的工作量。如果驱动程序仍有未完成的工作(例如,budget
已耗尽),则 poll 方法应准确返回 budget
。在这种情况下,NAPI 实例将被再次服务/轮询(无需调度)。
如果事件处理已完成(所有未完成的数据包已处理),则 poll 方法应在返回之前调用 napi_complete_done()
。napi_complete_done()
释放实例的所有权。
警告
必须谨慎处理完成所有事件并恰好使用 budget
的情况。没有办法向堆栈报告这种(罕见的)情况,因此驱动程序必须要么不调用 napi_complete_done()
并等待再次被调用,要么返回 budget - 1
。
如果 budget
为 0,则永远不应调用 napi_complete_done()
。
调用顺序¶
驱动程序不应假设调用的确切顺序。可以在没有驱动程序调度实例的情况下调用 poll 方法(除非该实例被禁用)。同样,即使 napi_schedule()
成功(例如,如果实例被禁用),也不能保证 poll 方法会被调用。
如 控制 API 部分所述 - napi_disable()
和后续对 poll 方法的调用仅等待实例的所有权被释放,而不是等待 poll 方法退出。这意味着驱动程序在调用 napi_complete_done()
后应避免访问任何数据结构。
调度和 IRQ 屏蔽¶
驱动程序应在调度 NAPI 实例后保持中断屏蔽 - 在 NAPI 轮询完成之前,任何进一步的中断都是不必要的。
必须显式屏蔽中断的驱动程序(与设备自动屏蔽 IRQ 不同)应使用 napi_schedule_prep()
和 __napi_schedule()
调用
if (napi_schedule_prep(&v->napi)) {
mydrv_mask_rxtx_irq(v->idx);
/* schedule after masking to avoid races */
__napi_schedule(&v->napi);
}
只有在成功调用 napi_complete_done()
后,才应取消屏蔽 IRQ
if (budget && napi_complete_done(&v->napi, work_done)) {
mydrv_unmask_rxtx_irq(v->idx);
return min(work_done, budget - 1);
}
napi_schedule_irqoff()
是 napi_schedule()
的一个变体,它利用了在 IRQ 上下文中调用的保证(无需屏蔽中断)。如果 IRQ 被线程化(例如启用 PREEMPT_RT
),则 napi_schedule_irqoff()
将回退到 napi_schedule()
。
实例到队列映射¶
现代设备每个接口有多个 NAPI 实例(struct napi_struct)。对于实例如何映射到队列和中断没有严格的要求。NAPI 主要是一种轮询/处理抽象,没有特定的用户面向语义。也就是说,大多数网络设备最终以非常相似的方式使用 NAPI。
NAPI 实例通常与中断和队列对(队列对是一组单个 Rx 和单个 Tx 队列)一一对应。
在不太常见的情况下,一个 NAPI 实例可能用于多个队列,或者 Rx 和 Tx 队列可以通过单个内核上的单独 NAPI 实例来服务。然而,无论队列分配如何,NAPI 实例和中断之间通常仍然存在 1:1 的映射。
值得注意的是,ethtool API 使用“通道”术语,其中每个通道可以是 rx
、tx
或 combined
。尚不清楚什么构成一个通道;推荐的解释是将通道理解为服务给定类型的队列的 IRQ/NAPI。例如,1 个 rx
、1 个 tx
和 1 个 combined
通道的配置有望利用 3 个中断、2 个 Rx 和 2 个 Tx 队列。
用户 API¶
用户与 NAPI 的交互取决于 NAPI 实例 ID。实例 ID 仅通过 SO_INCOMING_NAPI_ID
套接字选项对用户可见。目前无法查询给定设备使用的 ID。
软件 IRQ 合并¶
默认情况下,NAPI 不执行任何显式的事件合并。在大多数情况下,批处理是由设备完成的 IRQ 合并造成的。在某些情况下,软件合并很有用。
可以将 NAPI 配置为启用重新轮询计时器,而不是在处理完所有数据包后立即取消屏蔽硬件中断。gro_flush_timeout
网络设备的 sysfs 配置用于控制计时器的延迟,而 napi_defer_hard_irqs
控制 NAPI 在放弃并返回使用硬件 IRQ 之前连续空轮询的次数。
也可以使用 netlink 通过 netdev-genl 在每个 NAPI 的基础上设置上述参数。当与 netlink 一起使用并在每个 NAPI 的基础上配置时,上述参数使用连字符而不是下划线:gro-flush-timeout
和 napi-defer-hard-irqs
。
每个 NAPI 的配置可以通过用户应用程序以编程方式完成,也可以通过内核源代码树中包含的脚本完成:tools/net/ynl/cli.py
。
例如,使用脚本
$ kernel-source/tools/net/ynl/cli.py \
--spec Documentation/netlink/specs/netdev.yaml \
--do napi-set \
--json='{"id": 345,
"defer-hard-irqs": 111,
"gro-flush-timeout": 11111}'
类似地,可以使用 netlink 通过 netdev-genl 设置参数 irq-suspend-timeout
。此值没有全局 sysfs 参数。
irq-suspend-timeout
用于确定应用程序可以完全挂起 IRQ 的时间。它与 SO_PREFER_BUSY_POLL 结合使用,可以通过 EPIOCSPARAMS
ioctl 在每个 epoll 上下文的基础上设置。
忙轮询¶
忙轮询允许用户进程在设备中断触发之前检查传入的数据包。与任何忙轮询一样,它以 CPU 周期换取更低的延迟(NAPI 忙轮询的生产用途尚不清楚)。
可以通过在选定的套接字上设置 SO_BUSY_POLL
或使用全局 net.core.busy_poll
和 net.core.busy_read
sysctls 来启用忙轮询。还存在用于 NAPI 忙轮询的 io_uring API。
基于 epoll 的忙轮询¶
可以直接从对 epoll_wait
的调用触发数据包处理。为了使用此功能,用户应用程序必须确保添加到 epoll 上下文的所有文件描述符都具有相同的 NAPI ID。
如果应用程序使用专用的 acceptor 线程,则应用程序可以使用 SO_INCOMING_NAPI_ID 获取传入连接的 NAPI ID,然后将该文件描述符分发给工作线程。工作线程会将文件描述符添加到其 epoll 上下文中。这将确保每个工作线程都有一个具有相同 NAPI ID 的 FD 的 epoll 上下文。
或者,如果应用程序使用 SO_REUSEPORT,则可以插入 bpf 或 ebpf 程序,将传入连接分发给线程,以便每个线程仅获得具有相同 NAPI ID 的传入连接。必须小心处理系统可能具有多个网卡的情况。
为了启用忙轮询,有两种选择
可以使用以微秒为单位的时间设置
/proc/sys/net/core/busy_poll
,以忙循环等待事件。这是一个系统范围的设置,会导致所有基于 epoll 的应用程序在调用 epoll_wait 时进行忙轮询。这可能不是理想的,因为许多应用程序可能不需要进行忙轮询。使用最近内核的应用程序可以在 epoll 上下文文件描述符上发出 ioctl,以设置 (
EPIOCSPARAMS
) 或获取 (EPIOCGPARAMS
)struct epoll_params
:,用户程序可以定义如下
struct epoll_params {
uint32_t busy_poll_usecs;
uint16_t busy_poll_budget;
uint8_t prefer_busy_poll;
/* pad the struct to a multiple of 64bits */
uint8_t __pad;
};
IRQ 缓解¶
虽然忙轮询应该由低延迟应用程序使用,但类似的机制可用于 IRQ 缓解。
非常高的每秒请求应用程序(尤其是路由/转发应用程序,特别是使用 AF_XDP 套接字的应用程序)可能不希望在完成请求或一批数据包的处理之前被中断。
此类应用程序可以向内核承诺,它们将定期执行忙轮询操作,并且驱动程序应使设备 IRQ 永久屏蔽。此模式通过使用 SO_PREFER_BUSY_POLL
套接字选项启用。为避免系统行为异常,如果 gro_flush_timeout
在没有任何忙轮询调用的情况下经过,则会撤销该承诺。对于基于 epoll 的忙轮询应用程序,可以将 struct epoll_params
的 prefer_busy_poll
字段设置为 1,并发出 EPIOCSPARAMS
ioctl 以启用此模式。有关更多详细信息,请参阅上节。
忙轮询的 NAPI 预算低于默认值(这在正常忙轮询的低延迟意图下是有道理的)。但是,IRQ 缓解并非如此,因此可以使用 SO_BUSY_POLL_BUDGET
套接字选项调整预算。对于基于 epoll 的忙轮询应用程序,可以在 struct epoll_params
中将 busy_poll_budget
字段调整为所需的值,并使用 EPIOCSPARAMS
ioctl 在特定的 epoll 上下文上进行设置。有关更多详细信息,请参阅上节。
重要的是要注意,为 gro_flush_timeout
选择一个较大的值将延迟 IRQ,以实现更好的批量处理,但当系统未完全加载时会产生延迟。为 gro_flush_timeout
选择一个较小的值可能会导致设备 IRQ 和 softirq 处理干扰尝试进行忙轮询的用户应用程序。在考虑这些权衡时,应仔细选择此值。基于 epoll 的忙轮询应用程序可以通过为 maxevents
选择合适的值来减轻用户处理的程度。
用户可能需要考虑另一种方法,即 IRQ 挂起,以帮助处理这些权衡。
IRQ 挂起¶
IRQ 挂起是一种机制,其中当 epoll 触发 NAPI 数据包处理时,设备 IRQ 会被屏蔽。
当应用程序调用 epoll_wait 成功检索事件时,内核将延迟 IRQ 挂起计时器。如果内核在忙轮询时没有检索到任何事件(例如,因为网络流量级别下降),则禁用 IRQ 挂起,并使用上述 IRQ 缓解策略。
这允许用户在 CPU 消耗和网络处理效率之间取得平衡。
要使用此机制
每个 NAPI 的配置参数
irq-suspend-timeout
应设置为应用程序可以挂起其 IRQ 的最大时间(以纳秒为单位)。这是使用 netlink 完成的,如上所述。此超时作为安全机制,如果应用程序已停止,则重新启动 IRQ 驱动程序中断处理。应选择此值,使其涵盖用户应用程序需要从其对 epoll_wait 的调用处理数据的时间,并注意应用程序可以通过在调用 epoll_wait 时设置max_events
来控制它们检索的数据量。可以将 sysfs 参数或每个 NAPI 的配置参数
gro_flush_timeout
和napi_defer_hard_irqs
设置为较低的值。它们将用于在忙轮询未找到数据后延迟 IRQ。必须将
prefer_busy_poll
标志设置为 true。可以使用上述EPIOCSPARAMS
ioctl 完成此操作。如上所述,应用程序使用 epoll 触发 NAPI 数据包处理。
如上所述,只要对 epoll_wait 的后续调用将事件返回到用户空间,就会延迟 irq-suspend-timeout
并且禁用 IRQ。这允许应用程序在不受干扰的情况下处理数据。
一旦对 epoll_wait 的调用导致未找到任何事件,则会自动禁用 IRQ 挂起,并且 gro_flush_timeout
和 napi_defer_hard_irqs
缓解机制将接管。
预计 irq-suspend-timeout
的值将远大于 gro_flush_timeout
,因为 irq-suspend-timeout
应该在用户空间处理周期的持续时间内挂起 IRQ。
虽然严格来说没有必要使用 napi_defer_hard_irqs
和 gro_flush_timeout
来使用 IRQ 挂起,但强烈建议使用它们。
IRQ 中断挂起会导致系统在轮询模式和中断驱动的数据包传递之间切换。在繁忙期间,irq-suspend-timeout
会覆盖 gro_flush_timeout
并保持系统忙于轮询,但是当 epoll 未发现任何事件时,gro_flush_timeout
和 napi_defer_hard_irqs
的设置将决定下一步操作。
网络处理和数据包传递基本上有三种可能的循环
hardirq -> softirq -> napi 轮询;基本中断传递
timer -> softirq -> napi 轮询;延迟中断处理
epoll -> 忙轮询 -> napi 轮询;忙循环
如果设置了 gro_flush_timeout
和 napi_defer_hard_irqs
,循环 2 可以从循环 1 中获取控制权。
如果设置了 gro_flush_timeout
和 napi_defer_hard_irqs
,则循环 2 和 3 会相互“争夺”控制权。
在繁忙期间,irq-suspend-timeout
在循环 2 中用作计时器,这实际上会使网络处理偏向于循环 3。
如果没有设置 gro_flush_timeout
和 napi_defer_hard_irqs
,则循环 3 无法从循环 1 中获取控制权。
因此,建议设置 gro_flush_timeout
和 napi_defer_hard_irqs
,因为否则设置 irq-suspend-timeout
可能没有任何明显的效果。
线程化 NAPI¶
线程化 NAPI 是一种操作模式,它使用专用的内核线程而不是软件 IRQ 上下文进行 NAPI 处理。该配置是每个网络设备进行的,会影响该设备的所有 NAPI 实例。每个 NAPI 实例都会产生一个单独的线程(称为 napi/${ifc-name}-${napi-id}
)。
建议将每个内核线程绑定到单个 CPU,该 CPU 与处理中断的 CPU 相同。请注意,IRQ 和 NAPI 实例之间的映射可能并不简单(并且取决于驱动程序)。NAPI 实例 ID 的分配顺序与内核线程的进程 ID 相反。
线程化 NAPI 通过在 netdev 的 sysfs 目录中写入 0/1 到 threaded
文件来控制。
脚注