2. NAPI 数据结构:struct napi_struct 结构体详解、poll 函数指针、状态标志位
NAPI 的核心数据结构是 struct napi_struct,它串联了驱动、中断处理、轮询回调与内核网络子系统。理解该结构体的每个字段、poll 函数指针的契约以及状态标志位的切换逻辑,是后续分析收包路径与性能优化的基础。
2.1 struct napi_struct 结构体全景
该结构体定义在 <linux/netdevice.h> 中,是每个 NAPI 实例的“控制块”。下面列出核心字段及其作用:
| 字段 | 类型 | 作用描述 |
|---|---|---|
poll_list |
struct list_head |
将 NAPI 实例链接到全局 napi_hash 或 CPU 的 softnet_data 轮询链表上。 |
poll |
int (*poll)(struct napi_struct *, int) |
驱动注册的轮询回调函数,NAPI 核心通过此指针从驱动批量收取数据包。 |
weight |
int |
单次 poll 调用允许处理的最大数据包数量(NAPI 预算)。典型值为 64。 |
state |
unsigned long |
位图标志,记录 NAPI 实例的调度、禁用、挂起等状态。 |
gro_list |
struct list_head |
GRO(Generic Receive Offload)同流数据包链表,用于合并小包。 |
skb |
struct sk_buff * |
指向当前正在构建的 GRO 合并 skb,或用于其他临时用途。 |
dev |
struct net_device * |
关联的网络设备,标识该 NAPI 实例属于哪个网卡。 |
napi_id |
int |
用于 RPS(Receive Packet Steering)哈希的 ID,帮助将中断分布到多个 CPU。 |
irq |
unsigned int |
绑定的中断号,用于调试和 /proc/interrupts 统计。 |
关键设计思想:
struct napi_struct 将中断上下文与轮询上下文解耦。驱动在中断中仅触发 NAPI 调度,实际收包工作全部在 poll 回调中完成,从而避免中断处理占用过多 CPU 时间。
2.2 poll 函数指针:契约与实现模式
poll 是 NAPI 驱动必须实现的回调函数,其原型为:
int poll(struct napi_struct *napi, int budget);
该函数遵循严格的契约:
- 返回值:返回实际处理的包数量(小于等于
budget)。 - budget 语义:驱动单次
poll最多处理budget个数据包。典型值为napi->weight(通常 64)。 - 退出条件:
- 如果驱动处理完所有待收包且返回包数 <
budget,NAPI 核心会关闭轮询模式,重新启用硬件中断。 - 如果返回包数 ==
budget,表示仍有包待处理,NAPI 核心将保持轮询状态,并在后续软中断中再次调用poll。
- 如果驱动处理完所有待收包且返回包数 <
- 不可阻塞:
poll在软中断上下文中执行,不能睡眠或调用可能引起调度的函数。
典型驱动实现模板:
static int my_poll(struct napi_struct *napi, int budget) {
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int work_done = 0;
while (work_done < budget) {
struct sk_buff *skb = my_hw_receive(priv);
if (!skb)
break;
napi_gro_receive(napi, skb); // 送入 GRO 或协议栈
work_done++;
}
if (work_done < budget) {
// 没有更多包,关闭轮询,重新开启中断
napi_complete_done(napi, work_done);
my_enable_hw_interrupt(priv);
}
return work_done;
}
2.3 状态标志位:NAPI_STATE_SCHED 及其他
state 字段是一个 unsigned long 位图,每个位代表一种状态。内核通过 test_bit、set_bit、clear_bit 原子操作来修改这些标志,确保多核安全。
| 标志位 | 位值 | 含义 |
|---|---|---|
NAPI_STATE_SCHED |
0 | NAPI 实例已被调度到轮询队列中,正在或即将被 poll 处理。这是最核心的标志位。 |
NAPI_STATE_DISABLE |
1 | NAPI 被显式禁用(如设备 down 时),阻止任何新的调度。 |
NAPI_STATE_NPSVC |
2 | 用于 Netpoll 场景,表示 NAPI 实例被 Netpoll 服务占用。 |
NAPI_STATE_LISTED |
3 | NAPI 实例已加入全局 napi_hash 链表,可被查找。 |
NAPI_STATE_NO_BUSY_POLL |
4 | 禁止对该 NAPI 实例进行忙轮询(busy polling)。 |
NAPI_STATE_PREFER_BUSY_POLL |
5 | 优先使用忙轮询而非中断驱动,用于低延迟场景。 |
NAPI_STATE_THREADED |
6 | NAPI 使用内核线程(kthread)模式执行 poll,而非软中断。 |
性能关键点:
NAPI_STATE_SCHED 的原子测试与设置是 NAPI 调度路径上的热点。内核使用 napi_schedule_prep 函数检查并设置该位,若设置失败(表示已在调度中),则跳过重复调度,避免中断风暴。
2.4 NAPI 状态机与调度流程
下图展示了 NAPI 实例在中断触发、调度、轮询、完成之间的状态转换:
2.5 关键 API 与原子操作
NAPI 子系统提供一组内联函数来操作 struct napi_struct 的状态,这些函数在驱动和内核网络栈中高频使用:
| API 函数 | 作用 | 原子操作 |
|---|---|---|
napi_schedule_prep |
检查并设置 NAPI_STATE_SCHED,若已设置则返回 false |
test_and_set_bit |
__napi_schedule |
将 NAPI 实例加入当前 CPU 的 softnet_data->poll_list,并触发软中断 |
链表操作 + __raise_softirq_irqoff |
napi_schedule |
组合 napi_schedule_prep + __napi_schedule |
复合 |
napi_complete_done |
清除 NAPI_STATE_SCHED,并返回是否关闭轮询 |
clear_bit |
napi_disable |
设置 NAPI_STATE_DISABLE,并等待当前 poll 完成 |
set_bit + 忙等待 |
napi_enable |
清除 NAPI_STATE_DISABLE,允许重新调度 |
clear_bit |
调度路径热点代码:
static inline bool napi_schedule_prep(struct napi_struct *n) {
return !test_and_set_bit(NAPI_STATE_SCHED, &n->state);
}
static inline void napi_schedule(struct napi_struct *n) {
if (napi_schedule_prep(n))
__napi_schedule(n);
}
这段代码是 NAPI 收包路径上最频繁执行的逻辑之一。每次硬件中断都会调用 napi_schedule,但只有第一次会成功设置 NAPI_STATE_SCHED,后续中断直接返回,从而避免中断风暴。
2.6 多队列网卡与多个 NAPI 实例
现代多队列网卡(如 Intel ixgbe、Mellanox mlx5)通常为每个 RX 队列分配独立的 struct napi_struct 实例。每个队列绑定到特定 CPU,实现收包负载均衡:
这种 1:1:1 的映射关系(RX 队列 → NAPI 实例 → CPU)是现代高性能网卡的标准配置,避免了多 CPU 竞争同一个 NAPI 实例的 state 位和 poll_list 链表。
性能优化启示: 在调优时,可以通过
/proc/irq/<irq_num>/smp_affinity 调整中断亲和性,确保 NAPI 实例的 poll 回调始终在目标 CPU 上执行。同时,napi_struct 的 weight 值可通过 /sys/class/net/<iface>/gro_flush_timeout 等参数间接调整,影响单次轮询的包处理量。