2. NAPI数据结构:struct napi_struct 结构体详解

NAPI(New API)是 Linux 内核为高性能网络设备设计的轮询与中断混合驱动框架。其核心数据结构 struct napi_struct 是每个注册了 NAPI 轮询的网络设备或虚拟接口在内核中的“轮询控制块”。理解该结构体的每个成员变量,是掌握 NAPI 调度机制、配置轮询权重、以及后续吞吐量调优的基础。

核心认知: struct napi_struct 是 NAPI 轮询循环的“调度器上下文”。它不直接存储网络数据包,而是存储控制轮询行为的状态、回调函数指针、以及挂接在软中断处理链表上的节点信息。

2.1 结构体全景视图

以下 SVG 展示了 struct napi_struct 在内核网络栈中的位置及其与设备、软中断系统的关系:

struct napi_struct 在内核网络栈中的位置 struct napi_struct 与内核网络栈关系图 网络硬件设备 驱动中断处理 struct napi_struct 轮询控制块 NET_RX_SOFTIRQ 网络协议栈 说明: 1. 硬件中断触发驱动注册的中断处理函数。 2. 驱动在中断上下文中调用 napi_schedule(),将 napi_struct 挂入软中断队列。 3. 软中断 NET_RX_SOFTIRQ 遍历队列,调用 napi_struct->poll() 轮询收包。

2.2 核心成员变量详解

struct napi_struct 定义在 <linux/netdevice.h> 中。以下表格列出了驱动开发和调优中最关键的成员变量:

成员变量 类型 作用与调优意义
poll int (*poll)(struct napi_struct *, int budget) 轮询回调函数指针。驱动必须实现此函数,内核在软中断中调用它来收包。budget 参数限制本次轮询最多处理的包数。
state unsigned long NAPI 状态位图。通过位操作控制 NAPI 的启用、禁用、调度等状态。常用标志位见下文。
weight int 轮询权重。驱动初始化时设定,默认通常为 64。每次 poll 调用时,budget 参数值等于 weight。调优时通过修改 weight 控制 CPU 在轮询上的时间片。
list struct list_head 链表节点。用于将 napi_struct 挂接到软中断的轮询链表(softnet_data->poll_list)中,实现多设备轮询调度。
dev_list struct list_head 设备链表节点。用于将 napi_struct 挂接到网络设备的 napi_list 中,一个设备可关联多个 NAPI 实例(多队列网卡)。
gro_list struct list_head GRO(Generic Receive Offload)流链表。在轮询过程中,驱动将同流的数据包合并后放入此链表,减少上层协议栈处理次数。
skb struct sk_buff * 当前正在处理的 skb 指针。用于 GRO 合并时跟踪最后一个包,非所有驱动都使用。
napi_id int NAPI 实例 ID,用于 RPS(Receive Packet Steering)和跟踪调试。
irq unsigned int 关联的中断号。在中断触发 NAPI 调度时记录,用于调试和中断亲和性设置。
调优关键: weight 和 poll 函数内部的收包逻辑直接决定单次轮询的 CPU 消耗。weight 值过大可能导致软中断长时间占用 CPU,造成其他任务饥饿;过小则可能频繁触发中断,降低吞吐量。

2.3 state 成员:NAPI 状态位图

state 是一个位图变量,通过原子位操作(set_bit、test_bit、clear_bit)控制 NAPI 的生命周期。以下是关键状态位:

  • NAPI_STATE_SCHED(位 0):调度标志。当驱动调用 napi_schedule() 时置位,表示该 NAPI 实例已挂入软中断轮询链表。软中断处理完成后清除。
  • NAPI_STATE_DISABLE(位 1):禁用标志。调用 napi_disable() 时置位,阻止新的调度请求,等待当前轮询结束后彻底禁用。
  • NAPI_STATE_NPSVC(位 2):服务中标志。软中断开始调用 poll 时置位,防止同一 NAPI 实例被多个 CPU 同时轮询。
  • NAPI_STATE_HASHED(位 3):已哈希标志。表示该 NAPI 实例已加入全局哈希表,用于 RPS 和调试。
  • NAPI_STATE_NO_BUSY_POLL(位 4):禁止忙轮询标志。用于禁用该 NAPI 实例的 busy poll 特性。

以下 SVG 展示了 NAPI 状态机的典型转换流程:

NAPI 状态机转换流程 NAPI 状态机转换(基于 state 位图) NAPI_STATE_SCHED=0 NAPI_STATE_SCHED=1 NAPI_STATE_NPSVC=1 NAPI_STATE_DISABLE=1 napi_schedule() 软中断开始 poll 完成 napi_disable() napi_enable()

2.4 poll 回调函数:轮询的核心

poll 是驱动开发者必须实现的函数,其原型为:

int poll(struct napi_struct *napi, int budget);

该函数在软中断上下文(NET_RX_SOFTIRQ)中被调用,执行以下典型流程:

  1. 从硬件接收队列批量读取数据包:通过 DMA 或寄存器读取已接收的 skb。
  2. 调用 napi_gro_receive() 或 netif_receive_skb():将 skb 送入 GRO 合并或直接递交协议栈。
  3. 返回实际处理的包数量:返回值必须 ≤ budget。
  4. 判断是否继续轮询:如果返回值等于 budget,表示还有剩余包未处理,NAPI 保持调度状态,等待下一次软中断;如果返回值小于 budget,表示队列已空,驱动应调用 napi_complete() 清除 SCHED 标志并重新开启硬件中断。
常见错误: poll 函数返回 0 但未调用 napi_complete(),会导致 NAPI 永久挂起,设备无法再接收数据包。务必保证返回值与 napi_complete() 的调用逻辑一致。

2.5 weight 与 budget 的关系

weight 在驱动初始化时通过 netif_napi_add() 设置:

netif_napi_add(netdev, &napi, poll_func, weight);

内核在调用 poll 时,将 weight 作为 budget 参数传入。以下是不同 weight 值对吞吐量和延迟的影响:

weight 值 典型场景 吞吐量 延迟 CPU 占用
16 低延迟、小包场景 较低 低 低
64 通用默认值 中等 中等 中等
128 高吞吐、大包场景 高 较高 高
256 极端吞吐场景 极高 高 极高
调优建议: 对于 10G/25G 网卡,建议从 weight=64 开始,通过监控 /proc/net/softnet_stat 中的 processed 和 time_squeeze 字段,逐步调整 weight 值。time_squeeze 过高说明 budget 耗尽频繁,可适当增大 weight。

2.6 list 成员:软中断调度链表

list 是 struct list_head 类型,用于将 NAPI 实例挂接到每个 CPU 的 softnet_data->poll_list 中。当多个设备同时触发 NAPI 调度时,内核按以下规则遍历链表:

  • 公平轮询:每个 NAPI 实例依次获得一次 poll 调用机会,每次调用传入的 budget 为全局 net_rx_action 的 budget 除以链表长度。
  • 防止饿死:如果一个 NAPI 实例每次 poll 都返回等于 budget 的值,它会被重新添加到链表尾部,让其他设备有机会执行。
  • 多队列网卡:每个队列可以拥有独立的 napi_struct,通过 list 挂接到同一个 CPU 的 poll_list,实现队列级负载均衡。

以下 SVG 展示了多 NAPI 实例在软中断链表中的调度过程:

多 NAPI 实例在 poll_list 中的调度 多 NAPI 实例在 softnet_data->poll_list 中的调度 softnet_data poll_list 头 napi_struct 1 napi_struct 2 napi_struct 3 调度顺序:napi_struct 1 → napi_struct 2 → napi_struct 3 → 回到链表头,循环直到 budget 耗尽或所有队列空。

2.7 驱动初始化与注册流程

驱动在探测函数中完成 struct napi_struct 的初始化和注册,典型代码片段如下:

// 1. 分配 napi_struct(通常在 netdev_priv 中或静态分配)
struct napi_struct *napi = &priv->napi;

// 2. 初始化并注册
netif_napi_add(netdev, napi, my_poll_func, 64);

// 3. 在设备打开时启用
napi_enable(napi);

// 4. 中断处理函数中调度
if (likely(napi_schedule_prep(napi))) {
    __napi_schedule(napi);
}

// 5. 在设备关闭时禁用
napi_disable(napi);

// 6. 在驱动卸载时删除
netif_napi_del(napi);
注意事项: netif_napi_add() 必须在 register_netdev() 之前调用;napi_enable() 和 napi_disable() 必须配对使用,且 napi_disable() 可能休眠,不能在中断上下文调用。

2.8 总结:成员变量速查表

成员 驱动开发者关注点 调优者关注点
poll 实现收包逻辑,注意返回值与 napi_complete 的配合 通过 ftrace 跟踪 poll 执行时间
state 使用 napi_schedule_prep 检查状态,避免竞态 查看 /proc/net/softnet_stat 间接推断状态异常
weight 初始化时设定,与硬件队列深度匹配 核心调优参数,影响吞吐量与延迟平衡
list 无需直接操作,由 napi_schedule 管理 多队列场景下关注各 CPU poll_list 负载均衡
gro_list 调用 napi_gro_receive 时自动管理 GRO 合并效率影响吞吐量,可调整 gro_flush_timeout