2. NAPI数据结构:struct napi_struct 结构体详解
NAPI(New API)是 Linux 内核为高性能网络设备设计的轮询与中断混合驱动框架。其核心数据结构 struct napi_struct 是每个注册了 NAPI 轮询的网络设备或虚拟接口在内核中的“轮询控制块”。理解该结构体的每个成员变量,是掌握 NAPI 调度机制、配置轮询权重、以及后续吞吐量调优的基础。
核心认知:
struct napi_struct 是 NAPI 轮询循环的“调度器上下文”。它不直接存储网络数据包,而是存储控制轮询行为的状态、回调函数指针、以及挂接在软中断处理链表上的节点信息。
2.1 结构体全景视图
以下 SVG 展示了 struct napi_struct 在内核网络栈中的位置及其与设备、软中断系统的关系:
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 状态机的典型转换流程:
2.4 poll 回调函数:轮询的核心
poll 是驱动开发者必须实现的函数,其原型为:
int poll(struct napi_struct *napi, int budget);
该函数在软中断上下文(NET_RX_SOFTIRQ)中被调用,执行以下典型流程:
- 从硬件接收队列批量读取数据包:通过 DMA 或寄存器读取已接收的 skb。
- 调用
napi_gro_receive()或netif_receive_skb():将 skb 送入 GRO 合并或直接递交协议栈。 - 返回实际处理的包数量:返回值必须 ≤ budget。
- 判断是否继续轮询:如果返回值等于 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 实例在软中断链表中的调度过程:
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 |