3. 传统NAPI机制:中断与轮询的结合
NAPI(New API)是Linux内核为解决传统中断驱动网络在高吞吐场景下的“中断风暴”问题而设计的混合机制。其核心思想是:在低负载时使用中断唤醒处理,在高负载时切换为轮询模式,从而在延迟与吞吐之间取得平衡。本章将深入分析NAPI的调度流程、核心数据结构以及中断与轮询的切换条件。
3.1 NAPI的设计动机:中断风暴与瓶颈
在纯中断模式下,每个数据包到达都会触发一次硬件中断,导致CPU频繁上下文切换。当网络流量达到10Gbps以上时,中断频率可达每秒数百万次,CPU几乎全部用于处理中断,无法执行用户态应用,这种现象称为中断风暴。
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯中断 | 延迟低,响应快 | 高吞吐时CPU占用率极高 | 低负载、延迟敏感 |
| 纯轮询 | 高吞吐时CPU利用率可控 | 空闲时浪费CPU周期 | 持续高负载 |
| NAPI(混合) | 兼顾低延迟与高吞吐 | 实现复杂度较高 | 通用场景 |
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数量
};
- 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流程图展示其完整路径。
3.4 NAPI的注册与初始化
网络设备驱动在初始化时必须完成NAPI的注册。标准流程如下:
- 分配napi_struct:通常在设备私有数据结构中嵌入
struct napi_struct。 - 设置weight和poll函数:weight通常设为64,poll函数由驱动实现。
- 调用netif_napi_add():将NAPI实例注册到内核,关联到指定网络设备。
- 使能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和忙轮询优化 |
3.6 NAPI与GRO的协同
NAPI为GRO(Generic Receive Offload)提供了理想的执行环境。在 napi_gro_receive() 函数中,数据包首先尝试与已有的GRO流进行合并,只有在合并失败或需要flush时才生成最终的skb送入协议栈。这种协同设计使得NAPI轮询周期内可以完成大量数据包的合并操作,显著减少上层协议栈的处理开销。
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的协同:在轮询上下文中完成数据包合并,进一步提升效率。