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 实例,以实现并行轮询。
struct napi_struct 内存布局与字段关系 struct napi_struct 调度与状态 poll_list → 链接到 softnet_data 链表 state → 位图标志 (SCHED/DISABLE/MISSED) weight → 单次预算 (默认 64) sched_clock → 调度时间戳 回调与设备绑定 poll → 驱动轮询函数指针 dev → 所属 net_device napi_id → 唯一 ID (RPS 使用) irq → 绑定的中断号 数据路径与 GRO skb → 内部 skb 缓存 gro_hash → GRO 哈希表 (16 桶) gro_count → 当前 GRO 合并流数量 gro_bitmask → 活跃桶位图 硬件队列关联 rx_config → 接收队列配置 tx_config → 发送队列配置 (可选) poll_owner → 当前执行 poll 的 CPU llhw_ops → 低层硬件操作 (可选) 协同 协同

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 实例分流。
poll 函数调用流程与预算契约 net_rx_action() napi->poll(budget) 返回值 == budget ? 是 (满预算) 保持调度,下次继续 否 (数据耗尽) napi_complete() + 开中断 核心契约:满预算 → 继续轮询;小于预算 → 退出轮询,等待下次中断

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 调度丢失或重复调度。
NAPI 状态标志位转换图 初始 (无标志) 中断到来 SCHED (0x01) poll 执行 SCHED + HASHED napi_complete() 回到初始 napi_disable() DISABLE (0x02) MISSED 标志在 DISABLE 期间有中断丢失时设置,重新 enable 后触发一次补偿 poll

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 来实现。
多队列 NAPI 实例与 CPU 映射 网卡 (NIC) RX Queue 0 RX Queue 1 RX Queue 2 RX Queue 3 napi_struct 0 weight=64, irq=10 ← 上一章 📖 返回目录 下一章 →