3. 传统NAPI机制:中断与轮询的结合

NAPI(New API)是Linux内核为解决传统中断驱动网络在高吞吐场景下的“中断风暴”问题而设计的混合机制。其核心思想是:在低负载时使用中断唤醒处理,在高负载时切换为轮询模式,从而在延迟与吞吐之间取得平衡。本章将深入分析NAPI的调度流程、核心数据结构以及中断与轮询的切换条件。

3.1 NAPI的设计动机:中断风暴与瓶颈

在纯中断模式下,每个数据包到达都会触发一次硬件中断,导致CPU频繁上下文切换。当网络流量达到10Gbps以上时,中断频率可达每秒数百万次,CPU几乎全部用于处理中断,无法执行用户态应用,这种现象称为中断风暴。

模式 优点 缺点 适用场景
纯中断 延迟低,响应快 高吞吐时CPU占用率极高 低负载、延迟敏感
纯轮询 高吞吐时CPU利用率可控 空闲时浪费CPU周期 持续高负载
NAPI(混合) 兼顾低延迟与高吞吐 实现复杂度较高 通用场景
关键指标:NAPI的切换阈值通常由驱动层的预算(budget)控制,默认每次轮询最多处理64个数据包。当驱动在中断上下文中检测到仍有待处理数据包时,会关闭中断并切换到轮询模式。

3.2 NAPI的核心数据结构

NAPI机制围绕两个核心数据结构构建:napi_struct 和 softnet_data。前者代表一个NAPI实例,后者是每个CPU的软网络数据块。

3.2.1 napi_struct

每个NAPI实例对应一个网络设备或一组设备,其关键字段如下:

struct napi_struct {
    struct list_head    poll_list;      // 挂入CPU轮询链表的节点
    unsigned long       state;          // NAPI状态标志位
    int                 weight;         // 每次轮询最大处理包数(预算)
    int                 (*poll)(struct napi_struct *, int); // 轮询回调函数
    struct net_device   *dev;           // 关联的网络设备
    struct list_head    dev_list;       // 设备链表节点
    struct hlist_node   napi_hash_node; // 哈希表节点
    unsigned int        gro_count;      // GRO合并计数
    struct sk_buff      *skb;           // 当前处理的skb
    struct list_head    rx_list;        // GRO待处理列表
    int                 rx_count;       // GRO列表中的skb数量
};
状态标志位(state)说明:
  • NAPIF_STATE_SCHED:表示该NAPI实例已被调度,正在轮询或等待轮询。
  • NAPIF_STATE_MISSED:表示在轮询期间有新的中断到达,需要重新调度。
  • NAPIF_STATE_DISABLE:表示NAPI被禁用,不参与调度。

3.2.2 softnet_data

每个CPU维护一个 softnet_data 实例,用于管理该CPU上的NAPI轮询队列和统计信息:

struct softnet_data {
    struct list_head    poll_list;          // 待轮询的NAPI实例链表
    struct sk_buff_head process_queue;      // 待处理的skb队列
    struct napi_struct  *napi_list;         // 注册到该CPU的NAPI列表
    unsigned int        processed;          // 已处理包数统计
    unsigned int        time_squeeze;       // 预算耗尽次数
    unsigned int        cpu_collision;      // CPU冲突计数
    unsigned int        received_rps;       // RPS接收计数
    struct Qdisc        *output_queue;      // 输出队列
    struct Qdisc        **output_queue_tailp; // 输出队列尾指针
};

3.3 NAPI的调度流程

NAPI的完整调度流程分为三个阶段:中断触发 → 中断处理 → 轮询处理。下面通过SVG流程图展示其完整路径。

NAPI调度流程图 硬件中断到达 中断处理函数 启用NAPI? 传统中断处理 关闭中断 加入poll_list NET_RX_SOFTIRQ napi_poll() 轮询 预算耗尽? 重新开启中断 说明: 当预算耗尽时,NAPI保持中断关闭, 将自身重新加入poll_list等待下次轮询。

3.4 NAPI的注册与初始化

网络设备驱动在初始化时必须完成NAPI的注册。标准流程如下:

  1. 分配napi_struct:通常在设备私有数据结构中嵌入 struct napi_struct。
  2. 设置weight和poll函数:weight通常设为64,poll函数由驱动实现。
  3. 调用netif_napi_add():将NAPI实例注册到内核,关联到指定网络设备。
  4. 使能NAPI:在设备打开时调用 napi_enable()。
// 驱动初始化示例
static int mydev_open(struct net_device *dev) {
    struct my_priv *priv = netdev_priv(dev);

    // 注册NAPI,weight=64
    netif_napi_add(dev, &priv->napi, mydev_poll, 64);

    // 使能NAPI
    napi_enable(&priv->napi);

    // 开启硬件中断
    enable_irq(priv->irq);
    return 0;
}

// 轮询回调实现
static int mydev_poll(struct napi_struct *napi, int budget) {
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    int work_done = 0;

    // 从硬件接收队列中取出数据包
    while (work_done < budget) {
        struct sk_buff *skb = mydev_rx_packet(priv);
        if (!skb) break;

        // 将skb送入协议栈
        napi_gro_receive(napi, skb);
        work_done++;
    }

    // 如果未耗尽预算,关闭轮询并重新开启中断
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        enable_irq(priv->irq);
    }

    return work_done;
}

3.5 中断与轮询的切换条件

NAPI的切换逻辑由 napi_schedule() 和 napi_complete() 两个核心函数控制:

函数 触发时机 行为
napi_schedule() 中断处理函数中,检测到仍有数据包待处理 设置NAPIF_STATE_SCHED标志,将NAPI加入CPU的poll_list,触发NET_RX_SOFTIRQ软中断
napi_complete() 轮询函数返回work_done < budget时 清除NAPIF_STATE_SCHED标志,从poll_list移除,允许重新开启硬件中断
napi_complete_done() napi_complete的增强版本 额外处理GRO flush和忙轮询优化
常见陷阱:驱动开发者在轮询函数中必须正确判断预算是否耗尽。如果work_done达到budget,不能调用napi_complete(),否则会导致NAPI永久关闭中断,造成网络中断。此时应返回budget,让内核在下次软中断中继续轮询。

3.6 NAPI与GRO的协同

NAPI为GRO(Generic Receive Offload)提供了理想的执行环境。在 napi_gro_receive() 函数中,数据包首先尝试与已有的GRO流进行合并,只有在合并失败或需要flush时才生成最终的skb送入协议栈。这种协同设计使得NAPI轮询周期内可以完成大量数据包的合并操作,显著减少上层协议栈的处理开销。

NAPI与GRO协同处理流程 硬件RX队列 中断处理 napi_schedule GRO合并 协议栈处理 napi_poll循环 (最多budget次) napi_complete 关键路径: 中断→调度→GRO合并 →轮询→完成

3.7 性能调优参数

NAPI的行为可以通过以下内核参数进行调整:

参数 位置 默认值 说明
net.core.netdev_budget /proc/sys/net/core/ 300 每次软中断中所有NAPI实例的总预算
net.core.netdev_budget_usecs /proc/sys/net/core/ 2000 每次软中断的最大处理时间(微秒)
net.core.dev_weight /proc/sys/net/core/ 64 单个NAPI实例的默认weight
gro_flush_timeout 网络设备ethtool参数 0(关闭) GRO flush超时时间,用于减少延迟
调优建议:
  • 对于延迟敏感场景(如Redis、Memcached),适当降低 netdev_budget 和 dev_weight,减少轮询时间。
  • 对于吞吐优先场景(如文件服务器、视频流),可增大 netdev_budget 至600-1000,让NAPI在一次软中断中处理更多数据包。
  • 启用 gro_flush_timeout(如10000纳秒)可以在GRO合并等待时间与延迟之间取得平衡。

3.8 本章小结

NAPI机制通过中断与轮询的动态切换,有效解决了高吞吐场景下的中断风暴问题。其核心设计包括:

  • napi_struct:每个NAPI实例的抽象,包含状态、预算和轮询回调。
  • softnet_data:每CPU的调度队列,管理待轮询的NAPI实例。
  • napi_schedule/napi_complete:中断与轮询模式切换的关键函数。
  • 与GRO的协同:在轮询上下文中完成数据包合并,进一步提升效率。