2. NAPI 数据结构详解
NAPI 机制的核心数据结构是 struct napi_struct,它串联了设备驱动、中断处理、轮询调度和网络协议栈的上层接口。理解该结构体的每个字段、poll 函数指针的契约以及状态标志位的转换逻辑,是编写高性能 NAPI 驱动的基础。
2.1 struct napi_struct 结构体全景
该结构体定义在 <linux/netdevice.h> 中,是每个 NAPI 驱动实例的“控制块”。下面列出其核心字段及作用:
| 字段名 | 类型 | 作用描述 |
|---|---|---|
poll_list |
struct list_head |
将当前 napi_struct 链接到全局的 softnet_data->poll_list 链表中,供软中断遍历调度。 |
state |
unsigned long |
位图标志,记录 NAPI 实例的运行时状态(如是否被调度、是否禁用)。 |
weight |
int |
单次 poll 调用的最大处理预算(包数量),典型值为 64。 |
sched_clock |
u64 |
记录最近一次调度的时间戳,用于延迟预算和 CPU 亲和性调优。 |
poll |
poll_func * |
驱动实现的轮询回调函数指针,软中断通过此函数收取数据包。 |
dev |
struct net_device * |
指向所属的网络设备,用于将数据包上送至协议栈。 |
skb |
struct sk_buff * |
NAPI 内部使用的 skb 缓存,用于零拷贝或碎片重组。 |
gro_hash |
struct list_head [GRO_HASH_BUCKETS] |
通用接收分片(GRO)的哈希表,用于合并同流数据包。 |
napi_id |
int |
NAPI 实例的唯一 ID,用于 RPS(Receive Packet Steering)和跟踪。 |
irq |
unsigned int |
绑定的中断号,用于中断亲和性和调试。 |
关键设计原则: 一个
napi_struct 实例通常对应一个接收队列(RX Queue)或一组中断。多队列网卡会为每个队列创建独立的 NAPI 实例,以实现并行轮询。
2.2 poll 函数指针:契约与实现
poll 是驱动开发者必须实现的回调,其原型定义如下:
int (*poll)(struct napi_struct *napi, int budget);
该函数在软中断上下文(net_rx_action)中被调用,必须遵守以下严格契约:
- 预算(budget)约束:函数最多处理
budget个数据包(或描述符)。超过预算时必须停止,并返回实际处理的数量。 - 返回值语义:
- 返回
budget(即满预算)→ 表示仍有数据待处理,NAPI 实例会保持在调度链表中,下次软中断继续轮询。 - 返回小于
budget的值(含 0)→ 表示数据已耗尽,驱动应调用napi_complete()退出轮询模式,并重新启用中断。
- 返回
- 禁止睡眠:
poll在软中断中执行,不能调用可能睡眠的函数(如kmalloc(GFP_KERNEL)、mutex_lock)。 - 数据上送:通过
netif_receive_skb()或napi_gro_receive()将 skb 送入协议栈。
性能提示: 如果驱动在单次 poll 中总是返回满预算(budget),说明接收速率超过处理能力,应考虑增大 weight 或使用多队列/多 NAPI 实例分流。
2.3 状态标志位详解
state 字段是一个 unsigned long,每一位代表一个独立的状态。内核通过 set_bit()、test_bit()、clear_bit() 等原子操作来修改,确保多核安全。以下是关键标志位:
| 标志位 | 宏定义 | 位值 | 含义与触发场景 |
|---|---|---|---|
| NAPI_STATE_SCHED | BIT(0) |
0x01 | NAPI 实例已被调度到软中断的 poll_list 中。设置:napi_schedule();清除:napi_complete()。 |
| NAPI_STATE_DISABLE | BIT(1) |
0x02 | NAPI 被显式禁用(如设备 down、调优时临时关闭)。设置:napi_disable();清除:napi_enable()。 |
| NAPI_STATE_NPSVC | BIT(2) |
0x04 | 非抢占式服务标志,用于旧版内核的软中断保护,现代内核较少使用。 |
| NAPI_STATE_HASHED | BIT(3) |
0x08 | NAPI 实例已注册到全局哈希表(用于 RPS 和跟踪)。设置:napi_hash_add();清除:napi_hash_del()。 |
| NAPI_STATE_NO_BUSY_POLL | BIT(4) |
0x10 | 禁止忙轮询(busy polling),用于低延迟但 CPU 敏感的场景。 |
| NAPI_STATE_IN_BUSY_POLL | BIT(5) |
0x20 | 当前正在执行忙轮询,防止递归调用。 |
| NAPI_STATE_PREFER_BUSY_POLL | BIT(6) |
0x40 | 优先使用忙轮询而非中断,用于极端低延迟场景。 |
| NAPI_STATE_THREADED | BIT(7) |
0x80 | NAPI 工作在线程化软中断( threaded NAPI)模式,poll 在独立内核线程中执行。 |
| NAPI_STATE_MISSED | BIT(8) |
0x100 | 在 NAPI 禁用期间有中断丢失,重新启用后需要立即调度一次 poll 以补收数据。 |
并发安全注意: 所有状态位操作必须使用原子位操作 API(如
test_and_set_bit()),不能直接读写 state 字段。否则在 SMP 系统上会出现竞态条件,导致 NAPI 调度丢失或重复调度。
2.4 核心 API 与状态位联动
驱动开发者最常调用的 NAPI API 与状态位的对应关系如下:
| API 函数 | 设置标志 | 清除标志 | 典型调用位置 |
|---|---|---|---|
napi_schedule() |
SCHED | — | 中断处理程序下半部 |
napi_complete() |
— | SCHED | poll 函数返回 < budget 时 |
napi_disable() |
DISABLE | SCHED (如果设置) | 设备关闭、调优、热插拔 |
napi_enable() |
— | DISABLE | 设备开启、调优完成 |
napi_hash_add() |
HASHED | — | NAPI 注册时 |
napi_hash_del() |
— | HASHED | NAPI 注销时 |
napi_schedule_irqoff() |
SCHED | — | 中断已关闭的快速路径 |
调试技巧: 通过
/proc/net/softnet_stat 可以观察每个 CPU 的 NAPI 调度次数、poll 次数和超限次数。如果 processed 远大于 scheduled,说明单次 poll 处理了多个批次,这是 NAPI 正常工作的表现。
2.5 多队列与 NAPI 实例的映射关系
现代网卡(如 Intel ixgbe、Mellanox mlx5)支持多个接收队列,每个队列通常拥有独立的 NAPI 实例。这种设计带来的好处:
- 并行性:多个 CPU 可以同时处理不同队列的 poll,互不干扰。
- 亲和性:每个 NAPI 实例可以绑定到特定 CPU,减少缓存抖动。
- 隔离性:一个队列的拥塞不会阻塞其他队列的数据处理。
驱动初始化时的典型代码模式:
// 为每个接收队列创建 NAPI 实例
for (i = 0; i < adapter->num_rx_queues; i++) {
struct napi_struct *napi = &adapter->rx_ring[i].napi;
netif_napi_add(adapter->netdev, napi, my_poll_func, 64);
napi->irq = pci_irq_vector(adapter->pdev, i);
// 可选:设置 CPU 亲和性
irq_set_affinity_hint(napi->irq, cpumask_of(i % num_online_cpus()));
}
性能调优建议: 在 NUMA 系统中,应确保 NAPI 实例所在的 CPU 与对应队列的 DMA 内存位于同一 NUMA 节点。可以通过
irqbalance 或手动设置 /proc/irq/<irq>/smp_affinity 来实现。