2. struct napi_struct 结构体深度解析
NAPI 的核心调度单元由 struct napi_struct 描述。每个注册了 NAPI 的网卡驱动都会分配一个该结构体实例,它串联了中断下半部、轮询回调、设备亲和性以及内核网络栈的收包入口。理解该结构体的每个成员,是优化收包路径的前提。
2.1 结构体全景
以下代码取自 Linux 内核 <linux/netdevice.h>,为便于讲解,省略了部分调试与统计字段,保留核心成员:
struct napi_struct {
/* 用于将当前 napi 挂入 CPU 的 poll_list 链表 */
struct list_head poll_list;
/* 状态标志位,使用 unsigned long 的位操作 */
unsigned long state;
/* 一次 poll 调用最多处理的报文数 */
int weight;
/* 轮询回调函数,驱动必须实现 */
int (*poll)(struct napi_struct *napi, int budget);
/* 所属的网络设备 */
struct net_device *dev;
/* 挂入 gro_list 的已聚合数据包链表 */
struct list_head gro_list;
/* GRO 计数与时间戳,用于 flush 判断 */
struct sk_buff *skb;
u32 gro_count;
/* 中断亲和性与调度控制 */
int napi_id;
struct rcu_head rcu;
/* 硬件时间戳与 IRQ 映射 */
struct hrtimer timer;
unsigned int napi_idr;
};
关键认知:NAPI 结构体本质是一个“软中断上下文中的轮询控制器”。驱动不需要自己管理收包线程,内核通过
napi_schedule() 将本结构体挂入 CPU 的待调度链表,由软中断统一调度 poll 回调。
2.2 核心成员详解
| 成员 | 类型 | 作用 | 配置要点 |
|---|---|---|---|
poll_list |
struct list_head |
将本 napi 挂入当前 CPU 的 softnet_data.poll_list 链表 |
由 napi_schedule() 自动操作,驱动不应直接修改 |
state |
unsigned long |
位图标志,控制 NAPI 的调度状态(禁用、调度中、已挂起等) | 通过 napi_disable()/napi_enable() 修改 |
weight |
int |
单次 poll 调用的最大收包预算 | 通常为 64,GRO 场景可适当增大 |
poll |
函数指针 | 驱动实现的收包回调,内核软中断中调用 | 必须实现,返回实际处理的报文数 |
dev |
struct net_device * |
关联的网卡设备 | 在 netif_napi_add() 时绑定 |
gro_list |
struct list_head |
GRO 同流数据包链表 | 由 napi_gro_receive() 维护 |
2.3 state 位图:NAPI 的“运行状态寄存器”
state 字段以位操作管理 NAPI 生命周期,驱动与内核通过预定义掩码判断状态:
/* 内核预定义的状态位 */
enum napi_state {
NAPI_STATE_SCHED, /* Bit 0: 已被调度,等待 poll */
NAPI_STATE_MISSED, /* Bit 1: 调度期间又有新中断到达 */
NAPI_STATE_DISABLE, /* Bit 2: 被驱动禁用,不可调度 */
NAPI_STATE_NPSVC, /* Bit 3: 非优先服务,用于 busy poll */
NAPI_STATE_LISTED, /* Bit 4: 已挂入 poll_list */
NAPI_STATE_NO_BUSY_POLL,/* Bit 5: 禁止 busy poll */
NAPI_STATE_IN_BUSY_POLL,/* Bit 6: 正在 busy poll */
};
调度流程中的状态变迁:
- 中断到来 →
napi_schedule()设置NAPI_STATE_SCHED,将 napi 挂入poll_list。 - 软中断执行 → 清除
NAPI_STATE_SCHED,调用poll回调。 - 若
poll未用完 budget → 清除NAPI_STATE_SCHED,重新使能中断。 - 若
poll用完 budget → 保留NAPI_STATE_SCHED,再次挂入链表等待下一轮。 - 若
poll执行期间又有中断 → 设置NAPI_STATE_MISSED,避免丢失事件。
2.4 weight 与 poll:收包预算与回调契约
weight 决定了单次软中断中 poll 最多能处理的报文数。内核默认值为 64,驱动可在 netif_napi_add() 时指定:
/* 驱动初始化 NAPI 的典型代码 */
netif_napi_add(netdev, &priv->napi, napi_poll, 64);
poll 回调必须遵循以下契约:
- 返回值:实际处理的报文数(≤ budget)。
- 退出条件:若处理数 < budget,表示当前无更多报文,内核会清除
NAPI_STATE_SCHED并重新使能中断。 - 继续条件:若处理数 == budget,表示仍有报文待收,内核保留
NAPI_STATE_SCHED,将 napi 重新加入poll_list等待下一轮调度。 - 禁止睡眠:
poll在软中断上下文中执行,不能调用可能导致睡眠的函数。
常见误区:weight 不是越大越好。过大的 weight 会导致单次软中断耗时过长,增加网络延迟抖动。对于万兆网卡,建议 64~128;对于 25G/40G 网卡,可结合 GRO 适当提升至 256,但需实测 CPU 开销。
2.5 结构体关系与调度流程
下图展示了 napi_struct 与 CPU 软中断、设备驱动之间的核心关系:
2.6 配置建议与最佳实践
- weight 与 budget 的关系:内核软中断的全局 budget 由
net.core.netdev_budget控制(默认 300),每个 napi 的 weight 不应超过该值。 - 多队列场景:每个 RX 队列应独立分配一个
napi_struct,通过netif_napi_add()注册,并利用irq_set_affinity_hint()将中断绑定到不同 CPU。 - 动态调整:通过
/sys/class/net/<iface>/queues/rx-<n>/napi_defer_hard_irqs和gro_flush_timeout可微调 NAPI 的调度行为,降低高吞吐下的中断频率。 - 调试接口:
/proc/net/softnet_stat可观察每个 CPU 的 NAPI 调度次数、超限次数和挤压情况。
驱动开发检查清单:
- ✅ 在 probe 中调用
netif_napi_add()注册 napi,指定合理的 weight。 - ✅ 在中断处理函数中调用
napi_schedule()而非直接收包。 - ✅ 在
poll回调中调用napi_complete()退出轮询,并重新使能中断。 - ✅ 在 remove 中调用
netif_napi_del()清理资源。 - ✅ 使用
napi_disable()/napi_enable()控制设备开/关时的 NAPI 状态。