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 每次轮询可处理的最大数据包数量

内部流程

  1. 将 poll 函数指针赋值给 n->poll
  2. 将 weight 赋值给 n->weight
  3. 初始化 n->poll_list 链表节点(通过 INIT_LIST_HEAD)
  4. 设置 n->state 为 NAPIF_STATE_SCHED 未置位状态
  5. 初始化 n->gro_list 和 n->rx_count 等 GRO 相关字段
最佳实践: 对于现代内核(4.15+),驱动可以直接使用 netif_napi_add() 替代手动调用 napi_init(),因为 netif_napi_add() 内部已包含初始化逻辑。
napi_init() 内部结构初始化流程 struct napi_struct *n poll 函数指针 weight 权重值 n->poll = poll n->weight = weight INIT_LIST_HEAD(poll_list) NAPI 实例就绪

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

内部执行流程

  1. 调用 napi_init(napi, poll, weight) 完成基础初始化
  2. 将 napi->dev 指向传入的 dev 设备
  3. 将 napi 通过 list_add_tail() 添加到 dev->napi_list 链表尾部
  4. 设置 napi->napi_id 为设备标识
注意事项:
  • weight 值不能超过 NAPI_POLL_WEIGHT(通常为 64),否则内核会打印警告
  • 同一个 NAPI 实例不能重复注册到多个设备
  • 驱动卸载时必须调用 netif_napi_del() 从链表中移除
netif_napi_add() 注册流程 net_device *dev napi_struct *napi poll + weight 步骤1: napi_init(napi, poll, weight) — 基础初始化 步骤2: napi->dev = dev — 绑定网络设备 步骤3: list_add_tail(&napi->poll_list, &dev->napi_list) — 加入设备链表 注册完成

3.3 napi_enable():使能 NAPI 调度

napi_enable() 是 NAPI 进入可工作状态的最后一步。它清除 NAPIF_STATE_SCHED 标志位,允许软中断调度该 NAPI 实例进行轮询。

函数原型

void napi_enable(struct napi_struct *n);

参数解析

参数 类型 说明
n struct napi_struct * 已注册到设备的 NAPI 实例

内部执行流程

  1. 检查 n->state 是否已包含 NAPIF_STATE_SCHED 标志
  2. 使用 clear_bit(NAPIF_STATE_SCHED, &n->state) 清除调度标志
  3. 确保 n->poll_owner 为当前 CPU(SMP 安全检查)
  4. 如果 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 轮询的负载均衡。
NAPI 完整初始化序列 1 request_irq 2 netif_napi_add 3 napi_enable 4 hw_start 关键顺序约束 • 中断处理函数必须在 NAPI 注册之前注册 • napi_enable() 必须在硬件启动之前调用 • 硬件启动后立即可能产生中断,此时 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 签名。