3. NAPI 数据结构:napi_struct 结构体详解
NAPI 轮询机制的核心数据结构是 napi_struct,它位于 <linux/netdevice.h> 中。每个支持 NAPI 的网卡驱动都会为每个接收队列(或每个设备)分配一个 napi_struct 实例。该结构体将设备驱动、软中断处理以及轮询逻辑紧密关联在一起,是理解 NAPI 收包流程的基石。
3.1 napi_struct 核心字段
下表列出了 napi_struct 中最关键的字段及其作用:
| 字段名 | 类型 | 作用描述 |
|---|---|---|
poll_list |
struct list_head |
用于将 NAPI 实例链接到全局的 softnet_data->poll_list 链表中,供软中断遍历调度。 |
state |
unsigned long |
状态标志位集合,通过位操作管理 NAPI 实例的生命周期(如调度、禁用、挂起等)。 |
weight |
int |
单次轮询预算(budget),即一次 poll 回调最多处理的报文数量。通常由驱动初始化时设定(如 64)。 |
poll |
int (*poll)(struct napi_struct *, int) |
函数指针,指向驱动实现的轮询回调函数。软中断通过调用此函数从硬件队列中批量收取报文。 |
dev |
struct net_device * |
指向所属的网络设备(网卡)实例,用于关联设备上下文。 |
skb |
struct sk_buff * |
用于在 GRO(Generic Receive Offload)处理中暂存合并中的 sk_buff,非 GRO 场景下通常为 NULL。 |
list |
struct list_head |
用于将 NAPI 实例加入驱动自身的私有链表(如多队列网卡的 NAPI 列表)。 |
关键设计思想:
napi_struct 将中断下半部处理从“每中断一次处理一次”转变为“每轮询周期处理一批”,通过 weight 和 poll 的组合实现了收包速率与 CPU 开销之间的自适应平衡。
3.2 poll 函数指针详解
poll 是 NAPI 机制中驱动与内核协议栈之间的核心契约。软中断(net_rx_action)在轮询到某个 NAPI 实例时,会调用该函数指针。驱动开发者必须实现此函数,其典型逻辑如下:
- 从硬件 RX 描述符环中批量读取报文:通过 DMA 地址获取已接收的数据包。
- 构造
sk_buff并提交给协议栈:调用napi_gro_receive()或netif_receive_skb()将报文送入上层处理。 - 返回实际处理的报文数量:返回值不能超过传入的
budget(即napi_struct->weight)。 - 判断是否退出轮询:如果处理的报文数小于
budget,说明硬件队列已空,驱动应调用napi_complete()结束轮询并重新开启硬中断。
// 典型的驱动 poll 函数伪代码
static int my_napi_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int work_done = 0;
// 1. 从硬件 RX 环中批量取包,最多取 budget 个
while (work_done < budget) {
struct sk_buff *skb = my_hw_dequeue(priv);
if (!skb)
break; // 硬件队列已空
// 2. 提交 skb 到协议栈(GRO 路径)
napi_gro_receive(napi, skb);
work_done++;
}
// 3. 判断是否完成本轮轮询
if (work_done < budget) {
// 硬件队列已空,关闭 NAPI 调度,重新开启硬中断
napi_complete_done(napi, work_done);
my_hw_enable_interrupts(priv);
}
return work_done;
}
性能要点:
poll 函数应尽量保持轻量,避免在循环内执行高开销操作(如内存分配失败重试)。驱动通常会在 poll 之前通过 prefetch 指令预取下一个描述符的 DMA 数据,以隐藏内存访问延迟。
3.3 状态标志位:NAPI_STATE_SCHED 等
napi_struct->state 字段使用位图来管理 NAPI 实例的多种状态。内核定义了以下标准标志位:
| 标志位 | 位值 | 含义 | 典型操作 |
|---|---|---|---|
NAPI_STATE_SCHED |
0 | NAPI 已被调度,当前正在轮询或即将被轮询。这是最核心的标志位。 | napi_schedule() 设置;napi_complete() 清除。 |
NAPI_STATE_MISSED |
1 | 在 NAPI 处于 SCHED 状态期间,又有新中断到达,表示可能有报文被错过。 |
中断处理函数中若发现 SCHED 已置位,则设置此位。 |
NAPI_STATE_DISABLE |
2 | NAPI 被显式禁用,通常用于设备关闭或调优场景。 | napi_disable() 设置;napi_enable() 清除。 |
NAPI_STATE_NPSVC |
3 | 用于 Netpoll 场景,表示 NAPI 被 Netpoll 上下文持有。 | Netpoll 模块内部使用。 |
NAPI_STATE_LISTED |
4 | NAPI 实例已加入 softnet_data->poll_list 链表。 |
napi_schedule() 内部设置;napi_complete() 清除。 |
NAPI_STATE_NO_BUSY_POLL |
5 | 禁止对该 NAPI 实例进行忙轮询(busy polling)。 | 由 SO_BUSY_POLL 套接字选项控制。 |
NAPI_STATE_PREFER_BUSY_POLL |
6 | 优先使用忙轮询而非中断驱动。 | 用于低延迟场景的配置。 |
状态转换的核心逻辑:
- 当硬中断触发时,驱动调用
napi_schedule(),该函数会设置NAPI_STATE_SCHED标志,并将 NAPI 实例加入当前 CPU 的poll_list,然后触发软中断NET_RX_SOFTIRQ。 - 软中断
net_rx_action遍历poll_list,对每个 NAPI 实例调用其poll函数。 - 当
poll函数返回且实际处理量小于budget时,驱动调用napi_complete(),该函数清除NAPI_STATE_SCHED和NAPI_STATE_LISTED标志,并将 NAPI 实例从poll_list中移除。 - 如果在
SCHED状态下又有新中断到达,中断处理函数会设置NAPI_STATE_MISSED标志,确保在下次轮询结束时重新调度 NAPI,避免丢包。
竞态条件注意:
NAPI_STATE_SCHED 的测试与设置操作必须使用原子位操作(如 test_and_set_bit),以防止在 SMP(对称多处理)环境下多个 CPU 同时调度同一个 NAPI 实例导致的双重入问题。
3.4 NAPI 数据结构关系图
下图展示了 napi_struct 与软中断、设备驱动以及协议栈之间的协作关系:
3.5 多队列网卡中的 NAPI 实例布局
现代多队列网卡(如 Intel ixgbe、Mellanox mlx5)通常为每个 RX 队列分配独立的 napi_struct 实例。这种设计允许每个 CPU 核心独立处理一个队列的报文,实现收包处理的并行化。驱动在初始化时会为每个队列创建 NAPI 实例,并将其绑定到特定的 CPU(通过 irq_affinity 或 RPS 机制)。
// 多队列驱动中 NAPI 实例的典型初始化模式
struct my_rx_queue {
struct napi_struct napi;
// ... 其他队列相关字段
};
static int my_probe(struct pci_dev *pdev, ...)
{
struct my_priv *priv;
int i;
priv = kzalloc(sizeof(*priv), GFP_KERNEL);
for (i = 0; i < priv->num_rx_queues; i++) {
struct my_rx_queue *rxq = &priv->rx_queues[i];
// 初始化 NAPI,绑定 poll 函数,设置 weight
netif_napi_add(priv->netdev, &rxq->napi, my_poll, 64);
}
// ...
}
优化建议: 在多队列场景下,应确保每个 NAPI 实例的
weight 值与硬件 RX 描述符环的大小匹配。如果 weight 设置过大,可能导致单次轮询耗时过长,增加其他队列的调度延迟;设置过小,则可能频繁触发 napi_complete 和重新调度,增加上下文切换开销。