3. NAPI 核心数据结构:struct napi_struct 结构体详解
NAPI 机制的核心调度单元是 struct napi_struct。每个注册了 NAPI 的网络设备(或虚拟设备队列)都拥有一个该结构体的实例。它负责管理轮询状态、挂载待处理设备、绑定轮询回调以及控制流量反压。理解该结构体的每个字段,是编写高效 NAPI 驱动的基础。
关键认知:
napi_struct 是驱动与内核网络子系统之间的“契约”。驱动通过它告诉内核“我有数据要轮询”,内核通过它控制驱动“你该停下来了”。
3.1 结构体全景视图
以下 SVG 展示了 napi_struct 在 NAPI 调度路径中的核心位置及其与上下层模块的关系。
3.2 核心字段详解
| 字段名 | 类型 | 核心作用 | 驱动开发者关注点 |
|---|---|---|---|
| state | unsigned long |
NAPI 实例的状态标志位,使用位操作管理 | 检查 NAPI_STATE_SCHED 判断是否已在调度中 |
| poll_list | struct list_head |
将 NAPI 实例链接到全局或 per-CPU 的轮询链表 | 由 napi_schedule() 自动管理,驱动通常不直接操作 |
| poll | int (*poll)(struct napi_struct *, int budget) |
驱动实现的轮询回调函数指针 | 必须实现,返回处理的数据包数量,不能超过 budget |
| weight | int |
单次轮询允许处理的最大数据包预算 | 通常设置为队列长度或 64,过大会导致软中断饥饿 |
| dev_list | struct list_head |
将 NAPI 实例挂入所属设备的 NAPI 列表 | 设备初始化时通过 netif_napi_add() 自动关联 |
| napi_id | int |
用于跟踪和调试的唯一 ID | 内核内部使用,驱动通常只读 |
3.3 state 字段:状态机与位标志
state 是 NAPI 实例的“大脑”,通过位操作控制调度生命周期。内核定义了以下关键状态位:
- NAPI_STATE_SCHED (bit 0):核心标志。置位表示 NAPI 实例已被调度,正在或即将被轮询。清除表示轮询结束,可以重新触发调度。
- NAPI_STATE_MISSED (bit 1):在轮询期间又有新中断到达,但无法再次调度(因为 SCHED 已置位)。内核会在轮询结束后自动重新触发调度。
- NAPI_STATE_DISABLE (bit 2):表示 NAPI 被显式禁用(如设备 down)。此时
napi_schedule()不会生效。 - NAPI_STATE_NPSVC (bit 3):用于
netpoll场景,防止递归轮询。 - NAPI_STATE_LISTED (bit 4):表示该 NAPI 实例已加入设备的 NAPI 列表。
驱动开发技巧: 在
poll() 回调中,不要手动修改 state。使用 napi_complete_done() 来安全地清除 NAPI_STATE_SCHED 并重新启用中断。直接调用 clear_bit() 可能导致竞态条件。
3.4 poll_list:调度链表的挂载与遍历
poll_list 是 NAPI 调度系统的“传送带”。当驱动调用 napi_schedule() 时,内核将该 NAPI 实例挂入当前 CPU 的 softnet_data->poll_list。软中断处理函数 net_rx_action() 遍历该链表,依次调用每个 NAPI 实例的 poll() 回调。
以下 SVG 展示了 poll_list 在调度过程中的动态变化:
3.5 poll 与 weight:轮询契约
poll 是驱动开发者必须实现的函数指针,weight 是内核给予驱动的“单次预算”。两者共同构成了 NAPI 的流量反压契约:
- budget 参数:内核传入的
budget是全局预算(通常为 300),而napi->weight是每个 NAPI 实例的本地预算。poll()返回实际处理的数据包数量,不能超过weight。 - 返回值语义:返回
weight表示“我还有数据要处理,请继续调度我”;返回小于weight的值表示“我已经处理完了,可以休息了”。 - 反压触发:如果所有 NAPI 实例都返回
weight,内核会认为 CPU 过载,触发反压机制(如暂停该 CPU 的轮询)。
常见错误: 在
poll() 中返回 0 但实际还有数据未处理,会导致数据包丢失。返回大于 weight 的值会被内核截断,并可能触发调度异常。务必保证返回值与实际处理数量一致。
3.6 驱动初始化与注册流程
以下代码片段展示了驱动如何初始化 napi_struct 并注册到内核:
#include <linux/netdevice.h>
#include <linux/interrupt.h>
/* 驱动私有数据结构 */
struct my_priv {
struct napi_struct napi;
struct net_device *ndev;
/* ... 其他字段 ... */
};
/* 轮询回调实现 */
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;
/* 从硬件接收队列批量获取 sk_buff */
while (work_done < budget) {
struct sk_buff *skb = my_hw_dequeue(priv);
if (!skb)
break;
napi_gro_receive(napi, skb); /* 或 netif_receive_skb */
work_done++;
}
/* 判断是否处理完毕 */
if (work_done < budget) {
napi_complete_done(napi, work_done);
/* 重新使能硬件中断 */
my_enable_irq(priv);
}
return work_done;
}
/* 设备打开回调 */
static int my_open(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
/* 初始化 NAPI:关联 poll 回调,设置 weight */
netif_napi_add(ndev, &priv->napi, my_poll, 64);
/* 使能 NAPI */
napi_enable(&priv->napi);
/* 使能硬件中断 */
my_enable_irq(priv);
return 0;
}
/* 中断处理函数(上半部) */
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
struct my_priv *priv = dev_id;
/* 禁用中断,防止风暴 */
my_disable_irq(priv);
/* 调度 NAPI 轮询 */
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}
3.7 关键 API 速查表
| API 函数 | 作用 | 调用时机 |
|---|---|---|
netif_napi_add() |
初始化 napi_struct 并关联设备、回调、weight | 驱动 probe 或 open 阶段 |
napi_enable() |
清除 NAPI_STATE_DISABLE,使能调度 | 设备 up 时 |
napi_disable() |
设置 NAPI_STATE_DISABLE,等待当前轮询结束 | 设备 down 时 |
napi_schedule() |
触发 NAPI 调度(若未在调度中) | 中断上半部 |
napi_complete_done() |
标记轮询完成,清除 SCHED 标志 | poll() 回调末尾 |
napi_gro_receive() |
将 sk_buff 送入 GRO 合并引擎 | poll() 回调中 |
性能要点:
weight 的选择直接影响吞吐量和延迟的平衡。对于高速网卡(10Gbps+),建议 weight 设置为 64~128;对于低速或延迟敏感场景,可降低至 32。过大的 weight 会导致单次轮询占用 CPU 过久,增加其他网络流的延迟。
3.8 本章小结
struct napi_struct是 NAPI 调度的核心数据结构,通过state管理生命周期,通过poll_list参与调度链表。poll回调是驱动与内核的契约接口,必须严格遵循 budget 和返回值规则。weight是单次轮询的预算上限,直接影响 CPU 占用和反压行为。- 驱动开发者应使用
netif_napi_add()/napi_enable()/napi_schedule()/napi_complete_done()标准 API,避免直接操作位标志。