4. NAPI 注册与初始化:驱动与内核的握手

NAPI 机制的核心在于将网卡中断处理与轮询收包逻辑绑定。这个绑定过程通过 netif_napi_add 函数完成注册,并通过 napi_enable / napi_disable 控制 NAPI 实例的启用与停用。本章将深入分析这三个关键 API 的实现细节,并展示它们在典型网卡驱动初始化流程中的完整调用链路。

4.1 netif_napi_add:注册 NAPI 实例

netif_napi_add 是驱动向内核网络子系统注册一个 NAPI 实例的入口。该函数将驱动提供的轮询函数(poll function)与一个 napi_struct 结构体绑定,并将其添加到所在网络设备的 NAPI 列表中。

4.1.1 函数原型与参数

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 struct napi_struct * 待初始化的 NAPI 实例指针(通常嵌入在驱动私有结构体中)
poll 函数指针 驱动实现的轮询回调函数,内核在轮询模式下调用此函数收包
weight int 每次轮询可处理的最大包数(典型值为 64)

4.1.2 核心初始化流程

netif_napi_add 内部执行以下关键步骤:

  1. 初始化链表节点:将 napi->dev_list 链表节点初始化,后续用于挂接到设备的 NAPI 列表。
  2. 设置轮询函数与权重:将传入的 poll 函数指针赋值给 napi->poll,将 weight 赋值给 napi->weight。
  3. 初始化状态标志:将 napi->state 初始化为 0,表示 NAPI 实例尚未启用。
  4. 初始化 GRO 相关字段:清零 napi->gro_list 和 napi->gro_count,为通用接收卸载(GRO)做准备。
  5. 挂接到设备:将 NAPI 实例添加到 dev->napi_list 链表中。
关键点:netif_napi_add 只完成注册和初始化,不会使 NAPI 实例立即进入工作状态。驱动必须显式调用 napi_enable 来激活轮询能力。
netif_napi_add 内部初始化流程 netif_napi_add(dev, napi, poll, weight) ① 初始化链表节点 napi->dev_list ② 设置 poll 函数指针与 weight ③ 初始化 state 为 0(未启用) ④ 挂接到 dev->napi_list ⚠ 注册后未启用 需调用 napi_enable

4.2 napi_enable / napi_disable:启用与停用

这两个函数控制 NAPI 实例的生命周期状态。驱动在设备打开时调用 napi_enable,在设备关闭时调用 napi_disable。

4.2.1 napi_enable 实现分析

void napi_enable(struct napi_struct *napi)
{
    BUG_ON(!test_bit(NAPI_STATE_SCHED, &napi->state));
    clear_bit(NAPI_STATE_DISABLE, &napi->state);
    if (napi->poll)
        set_bit(NAPI_STATE_SCHED, &napi->state);
}

关键逻辑:

  • 清除禁用标志:将 NAPI_STATE_DISABLE 位清零,允许 NAPI 实例被调度。
  • 设置调度标志:将 NAPI_STATE_SCHED 位置位,表示 NAPI 实例已准备好参与轮询调度。
  • 原子操作:使用 test_bit / clear_bit / set_bit 保证并发安全。
设计意图:napi_enable 必须在驱动完成硬件初始化、中断注册之后调用。过早启用可能导致 NAPI 在硬件未就绪时被调度,引发空指针访问。

4.2.2 napi_disable 实现分析

void napi_disable(struct napi_struct *napi)
{
    might_sleep();
    set_bit(NAPI_STATE_DISABLE, &napi->state);
    while (test_and_set_bit(NAPI_STATE_SCHED, &napi->state))
        msleep(1);
    clear_bit(NAPI_STATE_SCHED, &napi->state);
}

关键逻辑:

  • 设置禁用标志:通知内核此 NAPI 实例即将停用。
  • 等待轮询结束:通过自旋等待确保当前正在执行的轮询函数完成。
  • 清除调度标志:最终将 NAPI_STATE_SCHED 清除,彻底停止调度。
  • 可睡眠:使用 might_sleep() 声明,表明此函数可能阻塞,不能在原子上下文中调用。
特性 napi_enable napi_disable
调用时机 设备打开、硬件初始化完成后 设备关闭、卸载驱动前
是否可睡眠 否(原子操作) 是(可能 msleep)
核心操作 清除 DISABLE 位,设置 SCHED 位 设置 DISABLE 位,等待 SCHED 位释放
并发安全性 位操作保证原子性 test_and_set_bit + 忙等待

4.3 设备驱动中的 NAPI 初始化流程

在真实网卡驱动(如 Intel igb、e1000e、Mellanox mlx5)中,NAPI 的初始化遵循一套标准化的流程。以下以典型 PCIe 网卡驱动为例,展示完整的初始化链路。

4.3.1 完整初始化步骤

  1. 分配驱动私有结构体:在 probe 函数中分配包含 napi_struct 的私有数据结构。
  2. 注册 NAPI 实例:调用 netif_napi_add,传入设备指针、NAPI 实例、轮询函数和权重。
  3. 硬件初始化:配置 DMA 描述符环、分配 RX/TX 缓冲区、设置 MAC 地址等。
  4. 注册中断处理函数:为网卡分配 MSI-X 中断向量,注册中断处理程序。
  5. 启用 NAPI:调用 napi_enable,使 NAPI 实例进入可调度状态。
  6. 开启硬件接收:设置网卡控制寄存器,允许数据包流入。

4.3.2 典型驱动代码片段

// 驱动私有结构体
struct my_priv {
    struct napi_struct napi;
    struct net_device *netdev;
    void __iomem *hw_base;
    // ... 其他字段
};

// probe 函数中的 NAPI 初始化
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct net_device *netdev;
    struct my_priv *priv;

    // 1. 分配 net_device 和私有结构体
    netdev = alloc_etherdev(sizeof(struct my_priv));
    priv = netdev_priv(netdev);

    // 2. 注册 NAPI,权重设为 64
    netif_napi_add(netdev, &priv->napi, my_poll, 64);

    // 3. 硬件初始化(DMA 环、缓冲区等)
    my_hw_init(priv);

    // 4. 注册中断
    request_irq(pdev->irq, my_interrupt, 0, "my_driver", priv);

    // 5. 启用 NAPI
    napi_enable(&priv->napi);

    // 6. 开启硬件接收
    my_hw_rx_enable(priv);

    return 0;
}

// 轮询函数实现
static int my_poll(struct napi_struct *napi, int budget)
{
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;

    // 从 RX 描述符环收包,最多处理 budget 个
    work_done = my_rx_process(priv, budget);

    if (work_done < budget) {
        // 收包完毕,关闭轮询,重新开启中断
        napi_complete_done(napi, work_done);
        my_enable_interrupts(priv);
    }

    return work_done;
}
网卡驱动 NAPI 初始化完整流程 ① alloc_etherdev + netdev_priv ② netif_napi_add(dev, napi, poll, 64) ③ 硬件初始化(DMA 环、缓冲区) ④ request_irq 注册中断处理 ⑤ napi_enable 激活 NAPI ⑥ 开启硬件接收(RX enable) ⚠ 顺序不可颠倒 先注册中断再启用 NAPI 最后开启硬件接收

4.3.3 初始化顺序的重要性

常见错误:部分驱动开发者为了简化代码,在硬件初始化完成前就调用 napi_enable。这会导致以下问题:
  • 中断触发后,NAPI 轮询函数可能访问未初始化的 DMA 描述符环,引发内核崩溃。
  • 在 napi_enable 之后、硬件接收开启之前,如果有遗留中断到达,轮询函数将空转,浪费 CPU 周期。
正确做法:严格按照“硬件初始化 → 中断注册 → NAPI 启用 → 硬件接收开启”的顺序执行。

4.4 多队列网卡的 NAPI 初始化

现代多队列网卡(如 Intel XL710、Mellanox ConnectX-5)为每个 RX 队列分配独立的 NAPI 实例,实现队列级别的并行收包。

4.4.1 多 NAPI 实例注册

// 为每个 RX 队列注册独立的 NAPI
for (int i = 0; i < num_queues; i++) {
    struct my_queue *q = &priv->queues[i];
    netif_napi_add(netdev, &q->napi, queue_poll, 64);
    napi_enable(&q->napi);
}
特性 单队列 NAPI 多队列 NAPI
NAPI 实例数 1 等于 RX 队列数
中断亲和性 单个 CPU 可绑定不同 CPU
并行度 低 高(多 CPU 并行收包)
轮询函数 处理所有队列 每个队列独立处理
适用场景 低端嵌入式设备 高性能服务器

4.4.2 队列与 NAPI 的绑定关系

多队列网卡 NAPI 与 RX 队列绑定关系 网卡硬件 RX Queue 0 RX Queue 1 RX Queue N NAPI 实例 0 poll: queue0_poll NAPI 实例 1 poll: queue1_poll NAPI 实例 N poll: queueN_poll CPU 0 中断亲和 CPU 1 中断亲和 CPU N 中断亲和

4.5 本章小结

  • netif_napi_add 完成 NAPI 实例的注册与初始化,将驱动轮询函数与内核调度框架绑定。
  • napi_enable 激活 NAPI 实例,使其可被内核调度;napi_disable 安全停用 NAPI,等待当前轮询结束。
  • 驱动初始化必须遵循严格的顺序:硬件初始化 → 中断注册 → NAPI 启用 → 硬件接收开启。
  • 多队列网卡为每个 RX 队列注册独立 NAPI 实例,实现 CPU 间的并行收包,提升吞吐量。