3. NAPI 初始化与注册

NAPI 的初始化与注册是设备驱动启用 NAPI 模式的第一步。驱动必须在探测(probe)阶段完成 NAPI 实例的创建、与网络设备的绑定以及使能操作,之后内核才能通过 NAPI 调度机制调用驱动的轮询回调。本章详细拆解 napi_init()、netif_napi_add()、napi_enable()/napi_disable() 四个核心接口,并给出完整的驱动初始化流程。

3.1 napi_init():NAPI 实例的底层初始化

napi_init() 是 NAPI 结构体最底层的初始化函数,通常在驱动分配 struct napi_struct 后立即调用。该函数完成以下工作:

  • 将 poll_list 链表头初始化为空链表
  • 将 state 字段清零,清除所有标志位
  • 初始化 timer(用于低延迟模式下的定时轮询)
  • 将 napi_id 置为 0(后续由 netif_napi_add() 分配)
函数原型:
void napi_init(struct napi_struct *napi);

参数 napi 指向驱动已分配的 NAPI 实例。该函数不分配内存,只做字段清零和链表初始化。

驱动开发者需要注意:napi_init() 不会将 NAPI 实例与任何网络设备关联,也不会使能 NAPI 调度。它只是一个“干净的起点”,后续必须通过 netif_napi_add() 完成绑定。

napi_init() 内部状态初始化 struct napi_struct *napi 初始化操作(4 项) INIT_LIST_HEAD(&napi->poll_list) napi->state = 0 timer_setup(&napi->timer, ...) napi->napi_id = 0

3.2 netif_napi_add():NAPI 与网络设备的绑定

netif_napi_add() 是 NAPI 初始化流程中最关键的注册函数。它将 NAPI 实例与 struct 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_list 链表中
napi struct napi_struct * 已通过 napi_init() 初始化的 NAPI 实例指针
poll 函数指针 驱动的轮询回调函数,内核在调度 NAPI 时调用此函数收取数据包
weight int 每次轮询允许处理的最大数据包数量,典型值为 64
weight 参数选择建议: 对于千兆网卡,weight 通常设为 64;万兆网卡可设为 128 或更高。weight 过小会导致频繁调度增加 CPU 开销,过大则可能造成单次轮询时间过长,影响其他软中断的响应。

netif_napi_add() 内部执行的关键步骤:

  1. 调用 napi_init() 确保 NAPI 实例已初始化(防御性编程)
  2. 将 napi->dev 指向 dev
  3. 将 napi->poll 指向驱动提供的 poll 函数
  4. 设置 napi->weight 为传入的 weight 值
  5. 将 NAPI 实例添加到 dev->napi_list 链表中
  6. 分配全局唯一的 napi_id,用于 IRQ 亲和性跟踪
netif_napi_add() 绑定流程 dev (net_device) napi_struct poll 回调 weight netif_napi_add() 核心逻辑 绑定 dev / poll / weight 绑定后的 NAPI 实例状态 napi->dev = dev napi->poll = poll napi->weight = weight 加入 dev->napi_list

3.3 napi_enable() / napi_disable():NAPI 的使能与禁用

NAPI 实例在绑定设备后默认处于禁用状态,必须调用 napi_enable() 才能参与内核调度。这两个函数控制 NAPI 实例的生命周期开关。

3.3.1 napi_enable()

函数原型:
void napi_enable(struct napi_struct *napi);

napi_enable() 执行的操作:

  • 清除 NAPI_STATE_SCHED 标志位,允许内核调度该 NAPI 实例
  • 清除 NAPI_STATE_NPSVC 标志位(非抢占安全上下文标志)
  • 如果 NAPI 实例配置了低延迟模式(busy poll),还会初始化相关定时器
注意: napi_enable() 必须在驱动使能硬件中断 之前 调用。如果先开中断再使能 NAPI,中断处理函数中调用 napi_schedule() 时 NAPI 可能尚未就绪,导致调度失败或状态不一致。

3.3.2 napi_disable()

函数原型:
void napi_disable(struct napi_struct *napi);

napi_disable() 执行的操作:

  • 设置 NAPI_STATE_DISABLE 标志位,阻止新的调度请求
  • 等待当前正在执行的 poll 回调完成(忙等待,会睡眠)
  • 清除 NAPI_STATE_SCHED 标志位
  • 如果启用了低延迟模式,删除相关定时器
特性 napi_enable() napi_disable()
调用时机 驱动探测完成、开中断之前 驱动卸载、关中断之后
是否可睡眠 否(原子上下文安全) 是(可能等待 poll 完成)
对调度的影响 允许 NAPI 被调度 阻止新调度,等待当前调度结束
典型调用者 驱动 probe / 恢复(resume) 驱动 remove / 挂起(suspend)

3.4 设备驱动中的 NAPI 初始化完整流程

将上述三个步骤串联起来,形成设备驱动中 NAPI 初始化的标准流程。以下是一个典型网卡驱动的 probe 函数中 NAPI 初始化的代码模板:

static int mynic_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *netdev;
    struct mynic_priv *priv;
    int ret;

    // 1. 分配 net_device
    netdev = alloc_etherdev(sizeof(struct mynic_priv));
    if (!netdev)
        return -ENOMEM;

    priv = netdev_priv(netdev);
    priv->pdev = pdev;
    priv->netdev = netdev;

    // 2. 初始化 PCI 设备、DMA、寄存器映射等硬件资源
    ret = mynic_hw_init(pdev);
    if (ret)
        goto err_free_netdev;

    // 3. 初始化 NAPI 实例(底层清零)
    napi_init(&priv->napi);

    // 4. 注册 NAPI:绑定设备、轮询回调、weight
    netif_napi_add(netdev, &priv->napi, mynic_poll, 64);

    // 5. 注册网络设备(此时 NAPI 仍处于禁用状态)
    ret = register_netdev(netdev);
    if (ret)
        goto err_del_napi;

    // 6. 使能 NAPI(必须在开中断之前)
    napi_enable(&priv->napi);

    // 7. 使能硬件中断(此时 NAPI 已就绪)
    mynic_irq_enable(priv);

    return 0;

err_del_napi:
    netif_napi_del(&priv->napi);
err_free_netdev:
    free_netdev(netdev);
    return ret;
}
流程要点:
  1. napi_init() → netif_napi_add() → napi_enable() 顺序不可颠倒
  2. napi_enable() 必须在 register_netdev() 之后、开中断之前调用
  3. 卸载时顺序相反:关中断 → napi_disable() → netif_napi_del() → unregister_netdev()
设备驱动 NAPI 初始化完整流程 ① alloc_etherdev() 分配 net_device ② mynic_hw_init() 硬件初始化 ③ napi_init() 初始化 NAPI 实例 ④ netif_napi_add() 绑定设备与回调 ⑤ register_netdev() 注册网络设备 ⑥ napi_enable() 使能 NAPI 调度 ⑦ mynic_irq_enable() 使能硬件中断

3.5 常见错误与避坑指南

错误 1:napi_enable() 在开中断之后调用
后果:中断处理函数中调用 napi_schedule() 时,NAPI 实例尚未使能,调度请求被忽略,数据包无法被轮询处理。
解决方案:严格按照“NAPI 使能 → 开中断”的顺序执行。
错误 2:netif_napi_add() 重复调用
后果:同一个 NAPI 实例被多次添加到 dev->napi_list,导致链表出现循环或重复节点,内核遍历时崩溃。
解决方案:每个 NAPI 实例只调用一次 netif_napi_add(),或在调用前检查 napi->dev 是否为空。
错误 3:卸载时未调用 napi_disable() 直接释放内存
后果:如果 NAPI 轮询正在执行,释放 NAPI 实例内存会导致内核访问已释放的内存,引发 use-after-free 错误。
解决方案:卸载顺序必须为 napi_disable() → netif_napi_del() → 释放内存。

3.6 本章小结

函数 核心作用 调用顺序
napi_init() 清零 NAPI 实例,初始化链表和定时器 第 1 步
netif_napi_add() 绑定设备、注册轮询回调、设置 weight 第 2 步
napi_enable() 使能 NAPI 调度,允许内核调用 poll 第 3 步(开中断前)
napi_disable() 禁用 NAPI 调度,等待当前 poll 完成 卸载时第 1 步(关中断后)