linux:NAPI

时间:2024-10-22 09:53:45

NAPI(New API,新API) 是 Linux 内核中的一种用于高效处理网络包的机制,旨在减少网络包处理时的 CPU 占用和中断开销,从而提高高负载下网络性能。NAPI 通过结合中断驱动轮询机制,优化了网络接收流程,使系统在处理大量网络流量时更加高效。

NAPI 的背景和动机

传统的网络包处理方式是基于中断的,当网卡接收到数据包时,它会产生一个中断通知 CPU 进行处理。虽然这种方式在低负载时工作得很好,但在高负载时,由于网卡会频繁产生中断,CPU 将被大量中断打断,导致性能下降。每次处理中断的开销(上下文切换、缓存失效等)可能超过处理网络包本身的时间,称为中断风暴

为了解决这一问题,NAPI引入了中断驱动和轮询机制相结合的模型。这样可以减少中断次数,在高负载时,通过轮询方式来批量处理数据包,减少中断带来的开销。

NAPI 的工作原理

NAPI 的核心思想是将网络数据包的处理从完全的中断驱动模式转换为混合中断和轮询相结合模式:

  1. 中断触发:在低负载的情况下,网卡接收到网络包时,仍会产生中断,驱动程序会响应中断来处理网络包。

  2. 中断禁用和轮询:当网络负载高时,网卡只会产生一次中断来通知有数据包,然后驱动程序进入轮询模式。轮询模式下,驱动程序会定期检查网卡是否有新数据包,而不是依赖中断。此时,中断被暂时禁用,以避免频繁的中断造成 CPU 负载过重。

  3. 轮询结束,恢复中断:当驱动程序在轮询过程中发现网卡中的数据包处理完毕后,会重新启用中断机制,以确保在低负载情况下系统可以回到中断驱动的方式。

NAPI 的主要优势

  1. 减少中断开销:通过在高负载情况下启用轮询,NAPI 可以大幅减少中断的频率,从而降低 CPU 的中断处理开销,特别是在网络流量非常大的情况下。

  2. 提高吞吐量:NAPI 通过批量处理网络包,而不是每接收一个包就触发一次中断,减少了上下文切换和缓存失效的频率,进而提高了系统的网络吞吐量。

  3. 动态调整:NAPI 在低负载和高负载之间可以动态调整工作方式。在低负载下使用中断模式以保持系统响应性,在高负载下使用轮询以减少中断开销。

NAPI 代码实现

在 Linux 网络驱动程序中,NAPI 是通过 napi_struct 结构体来实现的。每个支持 NAPI 的网卡驱动程序都需要实现该结构,并通过 NAPI 提供的接口函数进行注册和管理。

核心代码示例
  1. NAPI 结构体初始化:在驱动初始化时,首先需要初始化 NAPI 结构体并注册到内核中。
struct napi_struct {
	/* The poll_list must only be managed by the entity which
	 * changes the state of the NAPI_STATE_SCHED bit.  This means
	 * whoever atomically sets that bit can add this napi_struct
	 * to the per-cpu poll_list, and whoever clears that bit
	 * can remove from the list right before clearing the bit.
	 */
	struct list_head	poll_list;  // poll 列表

	unsigned long		state;  // NAPI 状态
	int			weight;  // 轮询时一次处理的最大包数量
	unsigned int		gro_count;
	// 驱动程序的轮询函数
	int			(*poll)(struct napi_struct *, int);
	...
}
/**
 *	netif_napi_add - initialize a napi context
 *	@dev:  network device
 *	@napi: napi context
 *	@poll: polling function
 *	@weight: default weight
 *
 * netif_napi_add() must be used to initialize a napi context prior to calling
 * *any* of the other napi related functions.
 */
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
		    int (*poll)(struct napi_struct *, int), int weight);
// 实际调用
netif_napi_add(adapter->netdev, &q_vector->napi,
		       igb_poll, 64);
  1. 启用 NAPI:当网卡接收到数据包时,驱动首先会禁用中断并启动 NAPI 的轮询机制:
// 网卡的硬中断注册的处理函数
static irqreturn_t igb_msix_ring(int irq, void *data)
{
	struct igb_q_vector *q_vector = data;

	/* Write the ITR value calculated from the previous interrupt. */
	/*
	 * ITR (Interrupt Throttling Rate): 中断节流率,指的是控制中断触发频率的一个参数。
	 * 在网络设备中,频繁的中断可能会导致CPU负载过高,所以ITR用于调整中断的频率,确保性能与资源利用率之间的平衡。
	 */
	igb_write_itr(q_vector);
    
    // 启动 NAPI 轮询处理
	napi_schedule(&q_vector->napi);

	return IRQ_HANDLED;
}
  1. NAPI 轮询函数
    在中断触发后,内核会调度NAPI的轮询函数,该函数会被内核反复调用,直到所有的数据包都被处理完。这是一种软中断机制,可以在较低的优先级上处理数据包,避免频繁的硬中断。

    • 轮询期间,内核会调用驱动程序注册的 poll 函数,驱动程序会从网卡缓冲区中取出数据包并传递给上层协议栈(如TCP/IP栈)。
    • 每次轮询只处理一定数量的数据包(通常由驱动程序设定,如budget参数),以避免影响到系统的其它任务。
/**
 *  igb_poll - NAPI Rx polling callback
 *  @napi: napi polling structure
 *  @budget: count of how many packets we should handle
 * 
 * 该函数使用 NAPI 机制批量处理网络数据包,同时还包括清理发送(Tx)和接收(Rx)队列的中断。
 * 它的整体逻辑是首先处理发送和接收的数据,如果未完成所有数据包的处理,就继续保持轮询状态;否则,退出轮询模式并重新启用中断。
 * 
 * 在中断触发后,内核会调度NAPI的轮询函数,该函数会被内核反复调用,直到所有的数据包都被处理完。
 * 这是一种软中断机制,可以在较低的优先级上处理数据包,避免频繁的硬中断。
 * 轮询期间,内核会调用驱动程序注册的 igb_poll 函数,驱动程序会从网卡缓冲区中取出数据包并传递给上层协议栈(如TCP/IP栈)。
 * 每次轮询只处理一定数量的数据包(通常由驱动程序设定,如budget参数),以避免影响到系统的其它任务。 
 * 
 **/
static int igb_poll(struct napi_struct *napi, int budget)
{
	struct igb_q_vector *q_vector = container_of(napi,
						     struct igb_q_vector,
						     napi);
	bool clean_complete = true;

#ifdef CONFIG_IGB_DCA
	if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
		igb_update_dca(q_vector);
#endif
    // 清理发送队列 (tx)
	if (q_vector->tx.ring)
		clean_complete = igb_clean_tx_irq(q_vector);

    // 清理接收队列 (Rx)
	if (q_vector->rx.ring)
		clean_complete &= igb_clean_rx_irq(q_vector, budget);

	/* If all work not completed, return budget and keep polling */
	/*
	 * 如果 clean_complete 为 false,表示还未完成所有的发送和接收处理工作,这时会返回 budget,意味着本轮未处理完的工作超出了 budget 限制,NAPI 将继续保持轮询状态,等待下一次轮询周期继续处理。
	 */
	if (!clean_complete)
		return budget;

	/* If not enough Rx work done, exit the polling mode */
	// 当数据包全部处理完毕时,调用 napi_complete()推出轮询并恢复中断。
	napi_complete(napi);
	// 重新启用网卡设备的中断,使网卡可以在下次数据到来时触发中断通知CPU处理。
	igb_ring_irq_enable(q_vector);

	return 0;
}

  1. 完成轮询:当数据包处理完毕时,调用 napi_complete() 恢复中断机制。
napi_complete(napi);

NAPI 和传统中断模式对比

特性 传统中断模式 NAPI 模式
中断处理 每个数据包都会触发一次中断 低负载时使用中断,高负载时禁用中断使用轮询
CPU 负载 高负载时容易引发中断风暴,CPU 负载重 高负载时通过轮询减少中断,降低 CPU 负载
性能 高负载时性能急剧下降 高负载时吞吐量更高,性能更稳定
适用场景 适合低流量网络 适合高流量和大规模数据传输

适用场景

NAPI 主要用于需要处理大量网络流量的场景,如数据中心服务器、高性能网络设备等。它可以显著提高在高负载下网络包处理的效率,同时避免频繁中断带来的性能下降。

总结

NAPI 是一种灵活且高效的网络处理机制,特别适用于高吞吐量、高并发的场景,如数据中心和高速路由器。通过结合中断和轮询,NAPI 能够在不同负载下优化网络设备的数据包处理效率,减轻中断风暴对系统性能的影响。