3. NAPI 初始化与注册:napi_init()、netif_napi_add()、napi_enable() 函数流程与参数解析
NAPI 机制的生命周期始于初始化与注册阶段。设备驱动必须依次调用 napi_init()(或结构体赋值)、netif_napi_add() 将 NAPI 实例挂入网络设备,最后通过 napi_enable() 使其进入可调度状态。这三个函数构成了 NAPI 从“创建”到“就绪”的完整链路。
核心要点: NAPI 实例的初始化顺序不可颠倒——先初始化结构体,再注册到设备,最后使能调度。任何一步缺失或顺序错误都会导致 NAPI 调度异常或内核崩溃。
3.1 napi_init():NAPI 结构体初始化
napi_init() 是一个内联函数,用于初始化 struct napi_struct 的核心字段。在 Linux 内核中,该函数通常定义在 <linux/netdevice.h> 中。
函数原型
static inline void napi_init(struct napi_struct *n,
int (*poll)(struct napi_struct *, int),
int weight);
参数解析
| 参数 | 类型 | 说明 |
|---|---|---|
n |
struct napi_struct * |
指向待初始化的 NAPI 实例指针 |
poll |
函数指针 | NAPI 轮询回调函数,驱动必须实现 |
weight |
int |
每次轮询可处理的最大数据包数量 |
内部流程
- 将
poll函数指针赋值给n->poll - 将
weight赋值给n->weight - 初始化
n->poll_list链表节点(通过INIT_LIST_HEAD) - 设置
n->state为NAPIF_STATE_SCHED未置位状态 - 初始化
n->gro_list和n->rx_count等 GRO 相关字段
最佳实践: 对于现代内核(4.15+),驱动可以直接使用
netif_napi_add() 替代手动调用 napi_init(),因为 netif_napi_add() 内部已包含初始化逻辑。
3.2 netif_napi_add():将 NAPI 注册到网络设备
netif_napi_add() 是驱动注册 NAPI 的核心接口,它将初始化后的 NAPI 实例绑定到指定的 net_device 结构体上,并加入设备的 NAPI 链表。
函数原型
void netif_napi_add(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int),
int weight);
参数解析
| 参数 | 类型 | 说明 |
|---|---|---|
dev |
struct net_device * |
所属网络设备,NAPI 实例将挂载到此设备 |
napi |
struct napi_struct * |
待注册的 NAPI 实例 |
poll |
函数指针 | 轮询回调,与 napi_init 中的 poll 相同 |
weight |
int |
轮询预算,通常为 64 |
内部执行流程
- 调用
napi_init(napi, poll, weight)完成基础初始化 - 将
napi->dev指向传入的dev设备 - 将
napi通过list_add_tail()添加到dev->napi_list链表尾部 - 设置
napi->napi_id为设备标识
注意事项:
weight值不能超过NAPI_POLL_WEIGHT(通常为 64),否则内核会打印警告- 同一个 NAPI 实例不能重复注册到多个设备
- 驱动卸载时必须调用
netif_napi_del()从链表中移除
3.3 napi_enable():使能 NAPI 调度
napi_enable() 是 NAPI 进入可工作状态的最后一步。它清除 NAPIF_STATE_SCHED 标志位,允许软中断调度该 NAPI 实例进行轮询。
函数原型
void napi_enable(struct napi_struct *n);
参数解析
| 参数 | 类型 | 说明 |
|---|---|---|
n |
struct napi_struct * |
已注册到设备的 NAPI 实例 |
内部执行流程
- 检查
n->state是否已包含NAPIF_STATE_SCHED标志 - 使用
clear_bit(NAPIF_STATE_SCHED, &n->state)清除调度标志 - 确保
n->poll_owner为当前 CPU(SMP 安全检查) - 如果 NAPI 实例之前被禁用,清除
NAPIF_STATE_DISABLE标志
关键机制:
napi_enable() 必须在中断使能之后、设备开始产生中断之前调用。典型调用顺序为:request_irq() → netif_napi_add() → napi_enable() → 开启设备 RX/TX
3.4 完整初始化序列与典型代码示例
以下是一个真实的网络驱动初始化片段,展示了三个函数的完整调用顺序:
static int mydev_open(struct net_device *dev)
{
struct mydev_priv *priv = netdev_priv(dev);
int ret;
/* 1. 注册中断处理函数 */
ret = request_irq(priv->irq, mydev_isr, 0, dev->name, dev);
if (ret)
return ret;
/* 2. 初始化并注册 NAPI */
netif_napi_add(dev, &priv->napi, mydev_poll, 64);
/* 3. 使能 NAPI 调度 */
napi_enable(&priv->napi);
/* 4. 开启硬件接收 */
mydev_hw_start(dev);
netif_start_queue(dev);
return 0;
}
3.5 参数配置对比与最佳实践
| 参数 | 推荐值 | 过高风险 | 过低风险 |
|---|---|---|---|
weight |
64(默认) | 软中断占用时间过长,导致其他任务饥饿 | 频繁触发轮询,增加上下文切换开销 |
poll 函数 |
每次调用处理 budget 个包 |
函数内耗时过长,阻塞软中断 | 返回过早,无法充分利用批量处理优势 |
| 多队列 NAPI | 每队列独立 NAPI 实例 | 过多实例增加调度开销 | 单实例无法利用多核并行 |
性能调优提示: 对于高速网卡(10Gbps+),建议每 RX 队列独立注册 NAPI 实例,并将
weight 值设置为 64~128。通过 /proc/irq/<irq>/smp_affinity 将中断绑定到不同 CPU,实现 NAPI 轮询的负载均衡。
3.6 常见错误与调试方法
- 重复注册: 对同一个 NAPI 实例多次调用
netif_napi_add()会导致链表节点重复,引发内核崩溃。应在驱动初始化时只调用一次。 - 忘记使能: 注册后未调用
napi_enable(),中断处理函数中调用napi_schedule()将无效,数据包无法被轮询处理。 - weight 值异常: 设置
weight为 0 或负数会导致内核 panic,因为napi_poll()中会做除零检查。 - 调试方法: 使用
cat /proc/net/softnet_stat查看 NAPI 调度统计;使用tracepoint napi:napi_poll跟踪轮询事件。
内核版本差异: 在 Linux 5.0+ 内核中,
netif_napi_add() 增加了 NAPI_F_NO_GRO 标志位参数,用于禁用 GRO 功能。驱动开发者需要根据内核版本适配 API 签名。