3. NAPI 的注册与初始化:netif_napi_add() 函数流程、napi_enable() 与 napi_disable() 的实现
NAPI 机制是 Linux 内核网络栈高性能收包的核心基石。在设备驱动能够使用 NAPI 进行轮询收包之前,必须完成两个关键步骤:注册 NAPI 实例(将 napi_struct 结构体与设备绑定)和启用 NAPI(允许调度器将设备加入轮询列表)。本章深入剖析 netif_napi_add()、napi_enable() 和 napi_disable() 三个核心函数的实现细节与性能考量。
3.1 NAPI 核心数据结构:napi_struct
在分析注册函数之前,必须先理解 napi_struct 结构体。它是 NAPI 调度器与设备驱动之间的契约对象,每个支持 NAPI 的网卡队列(或虚拟设备)都拥有一个独立的 napi_struct 实例。
| 字段 | 类型 | 说明 |
|---|---|---|
poll_list |
struct list_head |
用于将 NAPI 实例链接到 CPU 的 softnet_data->poll_list 链表中 |
state |
unsigned long |
状态位图,包含 NAPI_STATE_SCHED(已调度)、NAPI_STATE_DISABLE(禁用中)等标志 |
weight |
int |
单次轮询预算,即一次 poll 回调最多处理的报文数,典型值为 64 |
poll |
poll_func |
设备驱动的轮询回调函数指针,由驱动实现 |
dev |
struct net_device * |
指向所属网络设备 |
gro_list |
struct list_head |
GRO(Generic Receive Offload)同流报文链表头 |
napi_id |
int |
用于 RPS(Receive Packet Steering)的哈希 ID |
napi_struct 的 weight 值直接影响收包延迟与 CPU 占用率之间的平衡。weight 过小会导致频繁触发软中断,增加上下文切换开销;weight 过大会延长单次轮询时间,增加其他网络流的延迟。内核默认值 64 是经过广泛测试的折中方案。
3.2 netif_napi_add():注册 NAPI 实例
netif_napi_add() 是设备驱动初始化阶段调用的注册函数,它将驱动提供的 napi_struct 实例与 net_device 关联起来。该函数在 Linux 5.x 之后分为两个变体:netif_napi_add()(传统版本)和 netif_napi_add_weight()(显式指定 weight 版本)。
函数原型与参数
void netif_napi_add(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int),
int weight);
| 参数 | 说明 |
|---|---|
dev |
指向网络设备结构体,NAPI 实例将绑定到此设备 |
napi |
驱动预先分配的 napi_struct 实例指针 |
poll |
驱动实现的轮询回调函数,返回值为已处理报文数 |
weight |
轮询预算,必须 ≤ NAPI_POLL_WEIGHT(默认 64) |
函数执行流程
- 参数校验:检查 weight 是否超过
NAPI_POLL_WEIGHT(64),若超过则截断并打印警告。 - 初始化链表头:调用
INIT_LIST_HEAD(&napi->poll_list),将poll_list初始化为空链表。 - 设置回调与预算:将
napi->poll指向驱动传入的 poll 函数,napi->weight设为传入的 weight 值。 - 关联设备:
napi->dev指向dev,建立双向关联。 - 初始化 GRO 相关字段:初始化
gro_list链表头,重置gro_count等统计字段。 - 添加到设备链表:将
napi通过napi_list字段添加到dev->napi_list链表中,便于设备级别的 NAPI 管理。 - 设置 NAPI ID:为
napi->napi_id分配一个全局唯一的 ID,用于 RPS 哈希。
netif_napi_add() 不会修改 napi->state 字段。新注册的 NAPI 实例默认处于 未启用 状态,必须显式调用 napi_enable() 后才能被调度器识别。
3.3 napi_enable():激活 NAPI 调度
napi_enable() 是驱动在注册 NAPI 实例后、开启设备中断前必须调用的函数。它的核心作用是清除 NAPI_STATE_DISABLE 标志,使 NAPI 实例可以被软中断调度器加入轮询列表。
函数实现
void napi_enable(struct napi_struct *n)
{
BUG_ON(!test_bit(NAPI_STATE_SCHED, &n->state));
smp_mb__before_atomic();
clear_bit(NAPI_STATE_DISABLE, &n->state);
smp_mb__after_atomic();
}
流程解析
- 断言检查:使用
BUG_ON确保当前 NAPI 实例 没有 处于NAPI_STATE_SCHED状态。如果处于调度状态就启用,说明驱动逻辑存在竞态条件。 - 内存屏障:
smp_mb__before_atomic()确保之前的所有写操作在清除标志之前对其他 CPU 可见。 - 清除禁用标志:
clear_bit(NAPI_STATE_DISABLE, &n->state)原子地清除禁用位,允许调度器将本 NAPI 实例加入轮询列表。 - 内存屏障:
smp_mb__after_atomic()确保清除操作完成后再执行后续指令,防止指令重排导致中断处理函数看到不一致的状态。
napi_enable() 之后才开启硬件中断。如果先开中断再启用 NAPI,可能发生以下竞态:中断触发 → 驱动调用 napi_schedule() → 发现 NAPI_STATE_DISABLE 仍被置位 → 调度失败 → 报文永久丢失。
3.4 napi_disable():安全停用 NAPI
napi_disable() 是 napi_enable() 的逆操作,用于在设备卸载、链路 down 或调优场景中安全地停止 NAPI 轮询。它是一个 阻塞函数,会等待当前正在执行的 poll 回调完成。
函数实现
void napi_disable(struct napi_struct *n)
{
might_sleep();
set_bit(NAPI_STATE_DISABLE, &n->state);
while (test_and_set_bit(NAPI_STATE_SCHED, &n->state))
msleep(1);
while (test_bit(NAPI_STATE_SCHED, &n->state))
msleep(1);
clear_bit(NAPI_STATE_SCHED, &n->state);
}
流程解析
- 可睡眠检查:
might_sleep()在启用了内核调试选项时,检查当前上下文是否允许睡眠(不能在原子上下文调用)。 - 设置禁用标志:原子置位
NAPI_STATE_DISABLE,阻止新的napi_schedule()调用将本实例加入调度队列。 - 等待调度完成:使用
test_and_set_bit(NAPI_STATE_SCHED)尝试获取调度锁。如果SCHED位已被置位(表示 poll 正在执行或即将执行),则循环等待 1ms。 - 等待 poll 退出:获取
SCHED位后,继续等待直到SCHED位被 poll 回调清除(表示 poll 已返回)。 - 清除调度位:最后清除
NAPI_STATE_SCHED位,完成禁用流程。
napi_disable() 中的 msleep(1) 循环是不得已的等待机制。在高速网络设备卸载时,如果 poll 回调正在处理大量报文,禁用操作可能阻塞数十毫秒。驱动设计时应尽量在低负载时段执行禁用操作。
3.5 注册与启用的完整流程
下图展示了从 NAPI 实例分配到最终启用的完整生命周期,以及各阶段的状态变化:
3.6 多队列场景下的 NAPI 注册策略
现代网卡普遍支持多队列(RSS),每个队列通常对应一个独立的 NAPI 实例。驱动需要为每个队列调用一次 netif_napi_add(),并确保每个 NAPI 实例绑定到正确的 net_device。
| 场景 | NAPI 实例数 | 注册方式 | 性能考量 |
|---|---|---|---|
| 单队列网卡 | 1 | 驱动 probe 时注册 1 个 NAPI | 简单,无队列选择开销 |
| 多队列网卡(RSS) | 等于队列数 | 每个队列独立注册,通常绑定到不同 CPU | 需注意 IRQ 亲和性,避免多个 NAPI 争抢同一 CPU |
| 虚拟设备(bonding/bridge) | 1 或多个 | 虚拟设备可注册自己的 NAPI,或复用底层设备 | 避免 NAPI 嵌套调度导致优先级反转 |
probe() 函数中为所有队列预先分配 napi_struct 数组,然后循环调用 netif_napi_add()。每个 NAPI 实例的 weight 应保持一致,避免调度器出现不公平的轮询时间分配。
3.7 常见问题与调试方法
- NAPI 注册后无法调度:检查是否调用了
napi_enable()。未启用时napi_schedule()会因NAPI_STATE_DISABLE标志而直接返回 false。 - napi_disable() 死锁:如果在 poll 回调中直接调用
napi_disable(),会导致死锁(poll 持有 SCHED 位,disable 等待 SCHED 释放)。正确的做法是在 workqueue 或独立线程中执行禁用操作。 - weight 设置过大导致软中断饥饿:当 weight 超过 64 时,内核会打印警告并截断。如果驱动强制使用大 weight,可能导致其他网络流(如 SSH)长时间得不到处理。
- 调试接口:通过
/proc/net/softnet_stat可以观察每个 CPU 的 NAPI 调度统计,包括processed(已处理报文数)和time_squeeze(预算耗尽次数)。
netif_napi_add() 的 weight 参数允许大于 64,但内核会静默截断。5.10 之后增加了显式的 netif_napi_add_weight() 变体,并强化了参数校验。升级内核时需注意驱动兼容性。