09 NAPI 收包轮询机制与实现详解

09 NAPI 收包轮询机制与实现详解

1. 概述

NAPI(New API)是 Linux 网卡 收包路径 的核心抽象:用 短硬中断 + 软中断批量轮询 替代“每包一进中断”,在高 PPS 场景下降低 CPU 开销、提高吞吐。

项目 说明
头文件 include/linux/netdevice.h
实现 net/core/dev.c__napi_schedulenet_rx_actionnetif_napi_add_weight
与 socket 关系 NAPI 在 L2/L3 入口net/socket.crecvmsg 在更上层读已入队的 skb
驱动职责 实现 poll(),在 RX 中断里调用 napi_schedule()

2. 要解决的问题

2.1 纯中断收包的问题

1
每包 → 硬中断 → 中断上下文收包、上交协议栈
  • 中断次数 ≈ PPS,千兆/万兆时 CPU 耗在中断与上下文切换;
  • 中断里不宜做重逻辑,却又必须及时处理;
  • 多队列、多 CPU 时难以统一调度。

2.2 NAPI 的思路

1
2
3
硬中断:关中断/清状态 → napi_schedule()
软中断 NET_RX_SOFTIRQ:net_rx_action() → 驱动 poll() 批量收包(有 budget 上限)
收完或达到 budget:napi_complete() → 可再开中断

本质:中断 触发,收包 轮询,并由内核 统一限流


3. 核心数据结构

定义于 include/linux/netdevice.h

1
2
3
4
5
6
7
8
9
10
11
12
struct napi_struct {
struct list_head poll_list; /* 挂在 per-CPU poll 队列 */
unsigned long state; /* NAPI_STATE_* 状态位 */
int weight; /* 单次 poll 包数上限 */
int (*poll)(struct napi_struct *, int);
struct net_device *dev;
struct gro_list gro_hash[GRO_HASH_BUCKETS];
struct list_head rx_list;
struct hlist_node napi_hash_node;
unsigned int napi_id; /* busy poll 查找用 */
/* ... timer, thread, skb 等 */
};

3.1 状态位(节选)

状态 含义
NAPI_STATE_SCHED 已调度,poll 进行中或待执行
NAPI_STATE_MISSED schedule 时已在跑,需再次调度
NAPI_STATE_DISABLE 禁用中,napi_schedule 无效
NAPI_STATE_THREADED poll 在内核线程中执行
NAPI_STATE_IN_BUSY_POLL 用户态 busy poll 占用

3.2 驱动回调 poll

1
int (*poll)(struct napi_struct *napi, int budget);
参数/返回值 约定
budget 本次最多处理包数(通常 = napi->weight
返回值 实际处理包数;必须 ≤ budget
< budget 通常表示本轮收完 → 调用 napi_complete()
== budget 可能还有包 → 框架 repoll

默认权重:NAPI_POLL_WEIGHT = 64


4. 驱动侧 API

4.1 注册与生命周期

API 作用
netif_napi_add(dev, napi, poll) 注册 NAPI,weight=64
netif_napi_add_weight(dev, napi, poll, weight) 指定 weight
netif_napi_add_tx(dev, napi, poll) 仅 TX 队列用 NAPI
napi_enable(napi) 使能,允许 schedule
napi_disable(napi) 禁用并等待 poll 结束
netif_napi_del(napi) 从设备链表移除

netif_napi_add_weight() 实现要点(net/core/dev.c):

  • 设置 napi->pollnapi->weightnapi->dev
  • 加入 dev->napi_listnapi_hash(busy poll);
  • 可选创建 NAPI 内核线程dev->threaded)。

4.2 调度与完成

API 典型调用点
napi_schedule(napi) RX 硬中断下半部
napi_schedule_irqoff(napi) local_irq_disable()
napi_complete(napi) poll 返回 work < budget
napi_complete_done(napi, work) 带 GRO flush 等
napi_reschedule(napi) complete 后仍有包

napi_schedule() 逻辑:

1
2
if (napi_schedule_prep(n))   /* CAS 置 SCHED,保证单实例 */
__napi_schedule(n); /* 加入本 CPU softnet_data.poll_list,触发软中断 */

4.3 收包辅助 API

API 作用
napi_alloc_skb(napi, len) NAPI 上下文分配 skb
napi_gro_receive(napi, skb) 收包并尝试 GRO 聚合
napi_consume_skb(skb, budget) 释放 skb(知悉 NAPI 预算)
napi_get_frags() / napi_frags_* 页片段收包模式

5. 内核调度路径

5.1 流程图

协议栈napi->pollNET_RX_SOFTIRQnapi_schedule驱动 ISR网卡 RX 中断协议栈napi->pollNET_RX_SOFTIRQnapi_schedule驱动 ISR网卡 RX 中断loop[work < budget]IRQnapi_schedule(napi)poll_list + raise softirq__napi_poll(napi, weight)读描述符/DMAnapi_gro_receive(skb)napi_complete (若收完)

5.2 net_rx_action(软中断)

net/core/dev.cNET_RX_SOFTIRQ 处理函数:

  1. 将当前 CPU softnet_data.poll_list 移到本地链表;
  2. 循环对每个 napi_struct 调用 napi_poll()__napi_poll()napi->poll()
  3. 累计消耗 netdev_budget(默认 300)与 netdev_budget_usecs 时间片;
  4. 预算用尽则 time_squeeze++,剩余 NAPI 留待下次软中断;
  5. 若 poll_list 非空,再次 raise_softirq(NET_RX_SOFTIRQ)

5.3 __napi_poll 与 repoll

1
2
3
4
5
work = n->poll(n, weight);
if (work < weight)
return work; /* 驱动应已 napi_complete */
/* work == weight:可能还有包 */
*repoll = true; /* 加入 repoll 链表,本轮或下轮继续 */

6. 驱动模板代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct my_netdev_priv {
struct napi_struct napi;
/* ... */
};

static int my_netdev_poll(struct napi_struct *napi, int budget)
{
struct my_netdev_priv *priv = netdev_priv(napi->dev);
int work = 0;

while (work < budget) {
struct sk_buff *skb;

skb = my_hw_fetch_one_packet(priv);
if (!skb)
break;

napi_gro_receive(napi, skb);
work++;
}

if (work < budget)
napi_complete_done(napi, work);

return work;
}

static irqreturn_t my_netdev_isr(int irq, void *dev_id)
{
struct net_device *dev = dev_id;
struct my_netdev_priv *priv = netdev_priv(dev);

/* 清中断源、关 RX 中断(视硬件而定) */
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}

static int my_netdev_probe(...)
{
struct my_netdev_priv *priv = ...;

netif_napi_add(ndev, &priv->napi, my_netdev_poll);
napi_enable(&priv->napi);
/* 注册 IRQ ... */
}

注意

  • 不要在 poll 里睡眠;
  • work > budget 会触发内核告警;
  • 多 RX 队列:每队列一个 napi_struct + 独立 poll

7. 系统级调优参数

参数 默认(约) 路径/变量 含义
NAPI_POLL_WEIGHT 64 单 NAPI 单次 poll 上限
dev_rx_weight 64 sysctl net.core.dev_weight backlog NAPI 权重
netdev_budget 300 sysctl net.core.netdev_budget 一次软中断最多“消耗”的包预算
netdev_budget_usecs ~2ms@HZ=100 sysctl net.core.netdev_budget_usecs 软中断时间上限
netdev_max_backlog 1000 sysctl net.core.netdev_max_backlog per-CPU 输入队列长度

7.1 观测

1
2
3
4
cat /proc/net/softnet_stat
# 列含义含:总处理包数、time_squeeze(预算不够被迫结束的次数)等

ethtool -S eth0 # 驱动层 RX 统计

time_squeeze 持续偏高 → 可适当增大 netdev_budget 或优化驱动/CPU 亲和。


8. 扩展机制

机制 说明
GRO napi_gro_receive 在 poll 路径聚合小包
RPS 跨 CPU 分发软中断(process_backlog + IPI)
busy poll 用户态通过 napi_id 轮询,低延迟场景
NAPI threaded dev_set_threaded(),poll 在 kthread
XDP 可在驱动更早 drop/redirect,与 NAPI 正交

协议栈默认 per-CPU backlog 也用一个 NAPI:process_backlog()(非网卡驱动),处理未启用 RPS 时的输入队列。


9. RK3588 关联

说明
实现位置 NAPI 框架在 net/corestmmac/dwmac、r8169 等在 drivers/net/ethernet/
板载 GMAC DT 中 gmac + stmmac 驱动,probe 时 netif_napi_add,RX IRQ → napi_schedule
WiFi SDIO/USB 无线驱动同样使用 NAPI;上电由 net/rfkill/rfkill-wlan.c 配合
与 Thunder Boot 不改变 NAPI 接口本身

调试千兆吞吐时:驱动 poll + netdev_budget + 中断亲和(smp_affinity 需一并查看。


10. 源码索引

内容 路径
struct napi_struct、内联 API include/linux/netdevice.h
netif_napi_add_weight__napi_schedulenet_rx_action net/core/dev.c
sysctl net/core/sysctl_net_core.c
napi_alloc_skb net/core/skbuff.c / include/linux/skbuff.h

11. 相关文档


12. 小结

问题 答案
NAPI 是什么? 网卡收包的 调度框架(不是 socket API)
驱动要做什么? 实现 poll,中断里 napi_schedule,收完 napi_complete
内核做什么? 软中断里按 budget 批量调 poll,全局限流
为何重要? 高吞吐下 减中断、提效率、统一调度

文章互动

阅读 --

留言

0 条留言

正在加载留言…