NAPI In Linux Network Device Driver


简介: NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以 POLL 的方法来轮询数据,类似于底半方式(bottom-half 的处理模式);但是目前在 Linux 的 NAPI 工作效率比较差,本文在分析 NAPI 的同时,提供了一种高效的改善方式供大家参考。


NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据,(类似于底半(bottom-half)处理模式);从我们在实验中所得到的数据来看,在随着网络的接收速度的增加,NIC 触发的中断能做到不断减少,目前 NAPI 技术已经在网卡驱动层和网络层得到了广泛的应用,驱动层次上已经有 E1000 系列网卡,RTL8139 系列网卡,3c50X 系列等主流的网络适配器都采用了这个技术,而在网络层次上,NAPI 技术已经完全被应用到了著名的 netif_rx 函数中间,并且提供了专门的 POLL 方法--process_backlog 来处理轮询的方法;根据实验数据表明采用NAPI技术可以大大改善短长度数据包接收的效率,减少中断触发的时间;由于 RTL8139CP 是一种应用比较广泛的网络适配器,所以本文以其为例,说明了NAPI技术在网络适配器上的应用和基本原理。

但是 NAPI 存在一些比较严重的缺陷:而对于上层的应用程序而言,系统不能在每个数据包接收到的时候都可以及时地去处理它,而且随着传输速度增加,累计的数据包将会耗费大量的内存,经过实验表明在 Linux 平台上这个问题会比在 FreeBSD 上要严重一些;另外采用 NAPI 所造成的另外一个问题是对于大的数据包处理比较困难,原因是大的数据包传送到网络层上的时候耗费的时间比短数据包长很多(即使是采用 DMA 方式),所以正如前面所说的那样,NAPI 技术适用于对高速率的短长度数据包的处理,在本文的末尾提出了 NAPI 的改善方法,和实验数据。


驱动可以继续使用老的 2.4 内核的网络驱动程序接口,NAPI 的加入并不会导致向前兼容性的丧失,但是 NAPI 的使用至少要得到下面的保证:

A. 要使用 DMA 的环形输入队列(也就是 ring_dma,这个在 2.4 驱动中关于 Ethernet 的部分有详细的介绍),或者是有足够的内存空间缓存驱动获得的包。

B. 在发送/接收数据包产生中断的时候有能力关断 NIC 中断的事件处理,并且在关断 NIC 以后,并不影响数据包接收到网络设备的环形缓冲区(以下简称 rx-ring)处理队列中。

NAPI 对数据包到达的事件的处理采用轮询方法,在数据包达到的时候,NAPI 就会强制执行dev->poll 方法。而和不象以前的驱动那样为了减少包到达时间的处理延迟,通常采用中断的方法来进行。

应当注意的是,经过测试如果 DEC Tulip 系列(DE21x4x芯片)以及 National Semi 的部分网卡芯片,的测试表明如果把从前中断处理的部分都改换用设备的 POLL 方法去执行,那么会造成轻微的延迟,因此在进行 MII(介质无关)的操作上就需要一些小小的诀窍,详见 mii_check_media的函数处理流程,本文不做详细讨论。

在下面显示的例子表示了在 8139 中如何把处理过程放在 dev 的 poll 方法中,把所有的原来中断应该处理的过程放在了 POLL 方法里面,篇幅起见,我们只介绍接收的 POLL 方法。

在下面的 8139CP 驱动程序介绍中表明了可以把在中断程序中所做的任何事情放在 POLL 方法中去做,当然不同的 NIC 在中断中所要处理的状态和事件是不一样的。

对于所有的 NIC 设备,以下两种类型的 NIC 接收事件寄存器响应机制:

  1. COR 机制:当用户程序读状态/事件寄存器,读完成的时候寄存器和NIC的rx-ring中表示的状态队列将被清零,natsemi 和 sunbmac 的 NIC 会这样做,在这种情况下,必须把 NIC 所有以前的中断响应的处理部分都移动到 POLL 方法中去。
  2. COW 机制:用户程序写状态寄存器的时候,必须对要写的位先写 1 清 0,如下面要介绍的 8139CP 就是这样的类型,大多数的 NIC 都属于这种类型,而且这种类型对 NAPI 响应得最好,它只需要把接收的数据包处理部分放置在 POLL 方法中,而接收事件的状态处理部分放在原先的中断控制程序中,我们等下将要介绍的 8139CP 类型网卡就是属于这种类型。

C. 有防止 NIC 队列中排队的数据包冲突的能力。

当关断发送/接收事件中断的时候,NAPI 将在 POLL 中被调用处理,由于 POLL 方法的时候,NIC 中断已经不能通知包到达,那么这个时候在如果在完成轮询,并且中断打开以后,会马上有一个 NIC 中断产生,从而触发一次 POLL 事件,这种在中断关断时刻到达的包我们称为"rotting";这样就会在 POLL 机制和 NIC 中断之间产生一个竞争,解决的方法就是利用网卡的接收状态位,继续接收环形队列缓冲 rx-ring 中的数据,直到没有数据接收以后,才使能中断。


- 1.SMP 的保证机制:保证同时只有一个处理器调用网络设备的 POLL 方法,因为我们将在下面看到同时只有一个处理器可以对调用 netif_rx_schedule 挂在 POLL 队列中的 NIC 设备调用POLL 方法。

- 2. 网络核心层(net core)调用设备驱动程序使用循环方式发送数据包,在设备驱动层接收数据包的时候完全无锁的接收,而网络核心层则同样要保证每次只有一个处理器可以使用软中断处理接收队列。

- 3. 在多个处理器对 NIC 的 rx-ring 访问的时刻只能发生在对循环队列调用关闭(close)和挂起(suspend)方法的时候(在这个时刻会试图清除接收循环队列)

- 4. 数据同步的问题(对于接收循环队列来说),驱动程序是不需要考虑的网络层上的程序已经把这些事情做完了。

- 5. 如果没有把全部的部分交给 POLL 方法处理,那么 NIC 中断仍然需要使能,接收链路状态发生变化和发送完成中断仍然和以前的处理步骤一样,这样处理的假设是接收中断是设备负载最大的的情况,当然并不能说这样一定正确。

下面的部分将详细介绍在接收事件中调用设备的 POLL 方法。


struct softnet_data 结构内的字段就是 NIC 和网络层之间处理队列,这个结构是全局的,它从 NIC中断和 POLL 方法之间传递数据信息。其中包含的字段有:

struct softnet_data
{
	int			throttle;	/*为 1 表示当前队列的数据包被禁止*/
	int			cng_level;	/*表示当前处理器的数据包处理拥塞程度*/
	int			avg_blog;	/*某个处理器的平均拥塞度*/
	struct sk_buff_head	input_pkt_queue;	/*接收缓冲区的sk_buff队列*/
	struct list_head	         poll_list;	/*POLL设备队列头*/
	struct net_device	         output_queue; 	/*网络设备发送队列的队列头*/
	struct sk_buff		completion_queue; /*完成发送的数据包等待释放的队列*/
struct net_device	backlog_dev;	/*表示当前参与POLL处理的网络设备*/
};


1. netif_rx_schedule(dev)

这个函数被中断服务程序调用,将设备的 POLL 方法添加到网络层次的 POLL 处理队列中去,排队并且准备接收数据包,在使用之前需要调用 netif_rx_reschedule_prep,并且返回的数为 1,并且触发一个 NET_RX_SOFTIRQ 的软中断通知网络层接收数据包。

2. netif_rx_schedule_prep(dev)

确定设备处于运行,而且设备还没有被添加到网络层的 POLL 处理队列中,在调用 netif_rx_schedule之前会调用这个函数。

3. netif_rx_complete(dev)

把当前指定的设备从 POLL 队列中清除,通常被设备的 POLL 方法调用,注意如果在 POLL 队列处于工作状态的时候是不能把指定设备清除的,否则将会出错。


NAPI In Linux Network Device Driver_第1张图片
何在8139CP使用NAPI:
NAPI In Linux Network Device Driver_第2张图片

从 POLL 方法的本质意义上来说就在于尽量减少中断的数目,特别在于大量的小长度的数据包的时候,减少中断,以达到不要让整个操作系统花费太多的时间在中断现场的保护和恢复上,以便把赢得的时间用来在我网络层上的处理数据的传输,例如在下面介绍的 8139CP 中断的处理过程中,目的就在于尽快把产生中断的设备挂在 poll_list,并且关闭接收中断,最后直接调用设备的POLL方法来处理数据包的接收,直到收到数据包收无可收,或者是达到一个时间片内的调度完成。


RTL8139C+ 的接收方式是一种全新的缓冲方式,能显著的降低CPU接收数据造成的花费,适合大型的服务器使用,适合 IP,TCP,UDP 等多种方式的数据下载,以及连接 IEEE802.1P,802.1Q,VLAN等网络形式;在 8139CP 中分别有 64 个连续的接收/发送描述符单元,对应三个不同的环形缓冲队列--一个是高优先级传输描述符队列,一个是普通优先级传输符描述队列,一个是接收符描述队列,每个环形缓冲队列右 64 个4个双字的连续描述符组成,每个描述符有 4 个连续的双字组成,每个描述符的开始地址在 256 个字节的位置对齐,接收数据之前,软件需要预先分配一个 DMA 缓冲区,一般对于传输而言,缓冲区最大为 8Kbyte 并且把物理地址链接在描述符的 DMA 地址描述单元,另外还有两个双字的单元表示对应的 DMA 缓冲区的接收状态。

在 /driver/net/8139CP.C 中对于环形缓冲队列描述符的数据单元如下表示:

struct cp_desc {u32 opts1;/*缓冲区状态控制符,包含缓冲区大小,缓冲区传输启动位*/u32 opts2;/*专门用于VLAN部分*/u64 addr; /*缓冲区的DMA地址*/};

8139CP 的 NIC 中断:

static irqreturn_t
cp_interrupt (int irq, void *dev_instance, struct pt_regs *regs)
{
	struct net_device *dev = dev_instance;
	struct cp_private *cp = dev->priv;
	u16 status;
/*检查rx-ring中是否有中断到达*/
	status = cpr16(IntrStatus);
	if (!status || (status == 0xFFFF))
		return IRQ_NONE;
	if (netif_msg_intr(cp))
		printk(KERN_DEBUG "%s: intr, status %04x cmd %02x cpcmd %04x\n",
		        dev->name, status, cpr8(Cmd), cpr16(CpCmd));
/*清除NIC中断控制器的内容*/
	cpw16(IntrStatus, status & ~cp_rx_intr_mask);
	spin_lock(&cp->lock);
/*接收状态寄存器表示有数据包到达*/
	if (status & (RxOK | RxErr | RxEmpty | RxFIFOOvr)) {
/*把当前的产生中断的NIC设备挂在softnet_data中的POLL队列上,等待网络上层上的应用程序处理*/
		if (netif_rx_schedule_prep(dev)) {
/*关闭接收中断使能*/
			cpw16_f(IntrMask, cp_norx_intr_mask);
			__netif_rx_schedule(dev);
		}
	}
/*发送中断的处理过程以及8139C+的专门软中断的处理过程,这里我们不关心*/
	if (status & (TxOK | TxErr | TxEmpty | SWInt))
		cp_tx(cp);
/*如果发生链路变化的情况,需要检查介质无关接口(MII)的载波状态同样也发生变化,
否则就要准备重新启动MII接口*/
	if (status & LinkChg)
		mii_check_media(&cp->mii_if, netif_msg_link(cp), FALSE);
/*如果PCI总线发生错误,需要对8139C+的设备重新复位*/
	if (status & PciErr) {
		u16 pci_status;
		pci_read_config_word(cp->pdev, PCI_STATUS, &pci_status);
		pci_write_config_word(cp->pdev, PCI_STATUS, pci_status);
		printk(KERN_ERR "%s: PCI bus error, status=%04x, PCI status=%04x\n",
		       dev->name, status, pci_status);
		/* TODO: reset hardware */
	}
	spin_unlock(&cp->lock);
	return IRQ_HANDLED;
}


在 8139CP 的中断程序可以看到 __netif_rx_schedule 的调用方式,它把 NIC 设备挂在softnet_data 结构中的 poll_list 队列上,以便及时的返回中断,让专门数据包处理 bottom-half部分来进行处理,我们先来看一下 __netif_rx_schedule 的内部工作流程。

static inline void __netif_rx_schedule(struct net_device *dev)
{
	unsigned long flags;
	local_irq_save(flags);
	dev_hold(dev);
/*把当前NIC设备挂在POLL(poll_list)队列中,等待唤醒软中断以后进行轮询*/
	list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
/*确定当前该设备所要准备接收的包大小*/
	if (dev->quota < 0)
		dev->quota += dev->weight;
	else
		dev->quota = dev->weight;
/*启动软中断,在表示所有中断的状态字irq_cpustat_t中关于软中断字段__softirq_pending中,
把关于网络轮循接收软中断位置1,等待调度时机来临时候运行该中断的句柄net_rx_action。*/
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
	local_irq_restore(flags);
}

由 __netif_rx_schedule 启动的软中断的处理过程分析

软中断事件触发前已经在此设备子系统初始化时刻调用 subsys_initcall(net_dev_init) 在软中断控制台上被激活,挂在任务队列 tasklet 上准备在任务调度 schedule 的时刻运行它了,这个里面最主要的部分是调用了 8139C+ 网络设备的 POLL 方法(dev->poll),从网络设备的 rx-ring队列中获得数据,本来它应当放在网络设备中断服务程序中执行的,按照我们前面解释的那样,POLL方法以空间换取时间的机制把它放在软中断部分来执行轮循机制(采用类似老的 Bottom-half 机制也可以达到同样效果,而且更加容易理解一些)在每次进行进程调度的时候就会执行网络设备软中断,轮询 rx-ring 对 NIC 进行数据的接收。

软中断的处理过程:

static void net_rx_action(struct softirq_action *h)
{
	struct softnet_data *queue = &__get_cpu_var(softnet_data);
	unsigned long start_time = jiffies;
	int budget = netdev_max_backlog;/*表示队列的最大长度*/
/*锁定当前线程,多处理器的情况之下不能被其他处理器中断处理*/
	preempt_disable();
	local_irq_disable();
/*检查POLL队列(poll_list)上是否有设备在准备等待轮询取得数据*/
	while (!list_empty(&queue->poll_list)) {
		struct net_device *dev;
/*这里保证执行当前的 POLL 过程的时间不超过一个时间片,这样不至于被软中断占用太多的时间,
这样在一次调度的时间内执行完毕当前的 POLL 过程,budget 表示一个时间片内最大数据传输的"块数",
块的意思为每个 POLL 所完成 sk_buff数量,每块中间的 sk_buff 数量为 dev->quota 决定,在 8139CP 驱动中,
budget 为 300,而 quota 为 16 表示每给时间片最多可以接收到 4.8K 的 sk_buff 数量*/
		if (budget <= 0 || jiffies - start_time > 1)
			goto softnet_break;
		local_irq_enable();
/*从公共的 softnet_data 数据结构中的轮循队列上获得等待轮循的设备结构*/
		dev = list_entry(queue->poll_list.next,
				 struct net_device, poll_list);
/*调用设备的POLL方法从NIC上的Ring Buffer中读入数据*/
		if (dev->quota <= 0 || dev->poll(dev, &budget)) {
/*完成一次POLL过程的数据的接收,重新定义设备接收数据的"配额"
(事实上就是sk_buff缓冲区的数量,每次调用POLL方法的时候可以创建并且最
多可以向上层提交的sk_buff缓冲区数目,这个参数很重要在高速处理的时候有需要慎重优化这个数值,
在有大量数据接收的情况下,需要增加该数值)*/
			local_irq_disable();
			list_del(&dev->poll_list);
			list_add_tail(&dev->poll_list, &queue->poll_list);
			if (dev->quota < 0)
				dev->quota += dev->weight;
			else
				dev->quota = dev->weight;
		} else {
/*发生了错误的数据接收状况,或者没有完成"规定"配额的数据接收,并且没有新的数据进来,
这个也可能表示已经完成了传输的过程,调用__netif_rx_complete把网络设备从POLL队列上清除
(介绍POLL过程的时候详细介绍)*/
			dev_put(dev);
			local_irq_disable();
		}
	}
out:
	local_irq_enable();
	preempt_enable();
	return;
softnet_break:
	__get_cpu_var(netdev_rx_stat).time_squeeze++;
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
	goto out;
}


dev->poll 方法:

这个方法通常被网络层在向驱动的接收循环队列获取新的数据包时刻调用,而驱动的接收循环队列中可以向网络层交付的包数量则在 dev->quota 字段中表示,我们来看 8139cp 中 POLL 的原型:

static int cp_rx_poll (struct net_device *dev, int *budget)

参数 budget 的上层任务所需要底层传递的数据包的数量,这个数值不能超过netdev_max_backlog 的值。

总而言之,POLL 方法被网络层调用,只负责按照网络层的要求值("预算"值)提交对应数量的数据包。8139CP 的 POLL 方法注册通常在设备驱动程序模块初始化(调用 probe)的时候进行,如下:

static int cp_init_one (struct pci_dev *pdev, const struct pci_device_id *ent)
{
… …
dev->poll = cp_rx_poll;
… …
}

设备的 POLL 方法正如前所说的是被网络层上的软中断 net_rx_action 调用,我们现在来看具体的流程:

static int cp_rx_poll (struct net_device *dev, int *budget)
{
	struct cp_private *cp = netdev_priv(dev);
	unsigned rx_tail = cp->rx_tail;
	/*设定每次进行调度的时候从设备发送到网络层次最大的数据包的大小*/
unsigned rx_work = dev->quota;
	unsigned rx;
rx_status_loop:
	rx = 0;
/*重新打开NIC中断,在 cp_interrupt 中断句柄中中断关闭了,现在 POLl 已经开始处理环行缓冲队列中的数据,
所以中断可以打开,准备接收新的数据包*/
	cpw16(IntrStatus, cp_rx_intr_mask);
	while (1) {/*POLL循环的开始*/
		u32 status, len;
		dma_addr_t mapping;
		struct sk_buff *skb, *new_skb;
		struct cp_desc *desc;
		unsigned buflen;
/*从下标为rx_tail的内存中的环行缓冲队列接收队列rx_skb上"摘下"套接字缓冲区*/
		skb = cp->rx_skb[rx_tail].skb;
		if (!skb)
			BUG();
		desc = &cp->rx_ring[rx_tail];
/*检查在 NIC 的环形队列(rx_ring)上的最后的数据接收状态,是否有出现接收或者 FIFO 的错误,是否*/
		status = le32_to_cpu(desc->opts1);
		if (status & DescOwn)
			break;
		len = (status & 0x1fff) - 4;
		mapping = cp->rx_skb[rx_tail].mapping;
		if ((status & (FirstFrag | LastFrag)) != (FirstFrag | LastFrag)) {
			/* we don't support incoming fragmented frames.
			 * instead, we attempt to ensure that the
			 * pre-allocated RX skbs are properly sized such
			 * that RX fragments are never encountered
			 */
			cp_rx_err_acct(cp, rx_tail, status, len);
			cp->net_stats.rx_dropped++;
			cp->cp_stats.rx_frags++;
			goto rx_next;
		}
		if (status & (RxError | RxErrFIFO)) {
			cp_rx_err_acct(cp, rx_tail, status, len);
			goto rx_next;
		}
		if (netif_msg_rx_status(cp))
			printk(KERN_DEBUG "%s: rx slot %d status 0x%x len %d\n",
			       cp->dev->name, rx_tail, status, len);
		buflen = cp->rx_buf_sz + RX_OFFSET;
/*创建新的套接字缓冲区*/
		new_skb = dev_alloc_skb (buflen);
		if (!new_skb) {
			cp->net_stats.rx_dropped++;
			goto rx_next;
		}
		skb_reserve(new_skb, RX_OFFSET);
		new_skb->dev = cp->dev;
/*解除原先映射的环行队列上的映射区域*/
		pci_unmap_single(cp->pdev, mapping,
				 buflen, PCI_DMA_FROMDEVICE);
/*检查套接字缓冲区(sk_buff)上得到的数据校验和是否正确*/
		/* Handle checksum offloading for incoming packets. */
		if (cp_rx_csum_ok(status))
			skb->ip_summed = CHECKSUM_UNNECESSARY;
		else
			skb->ip_summed = CHECKSUM_NONE;
/*按照数据的实际大小重新定义套接字缓冲区的大小*/
		skb_put(skb, len);
		mapping =
		cp->rx_skb[rx_tail].mapping =
/*DMA影射在前面新创建的套接字缓冲区虚拟地址new_buf->tail到实际的物理地址上,
并且把这个物理地址挂在接收缓冲区的队列中*/
			pci_map_single(cp->pdev, new_skb->tail,
				       buflen, PCI_DMA_FROMDEVICE);
/*把新建立的缓冲区的虚拟地址挂在接收缓冲区的队列中,在下一次访问rx_skb数组的这个结构时候,
POLL方法会从这个虚拟地址读出接收到的数据包*/
		cp->rx_skb[rx_tail].skb = new_skb;
/*在cp_rx_skb调用netif_rx_skb,填充接收数据包队列,等待网络层在Bottom half队列中调用ip_rcv接收网络数据,
这个函数替代了以前使用的netif_rx*/
		cp_rx_skb(cp, skb, desc);
		rx++;
rx_next:
/*把前面映射的物理地址挂在NIC设备的环行队列上(也就是rx_ring上,它是在和NIC中物理存储区进行了DMA映射的,
而不是驱动在内存中动态建立的),准备提交给下层(NIC)进行数据传输*/
		cp->rx_ring[rx_tail].opts2 = 0;
		cp->rx_ring[rx_tail].addr = cpu_to_le64(mapping);
/*在相应的传输寄存器中写入控制字,把rx_ring的控制权从驱动程序交还给NIC硬件*/
		if (rx_tail == (CP_RX_RING_SIZE - 1))
			desc->opts1 = cpu_to_le32(DescOwn | RingEnd |
						  cp->rx_buf_sz);
		else
			desc->opts1 = cpu_to_le32(DescOwn | cp->rx_buf_sz);
/*步进到下一个接收缓冲队列的下一个单元*/
		rx_tail = NEXT_RX(rx_tail);
		if (!rx_work--)
			break;
	}
	cp->rx_tail = rx_tail;
/*递减配额值quota,一旦quota递减到0表示这次的POLL传输已经完成了使命,
就等待有数据到来的时候再次唤醒软中断执行POLL方法*/
	dev->quota -= rx;
	*budget -= rx;
	/* if we did not reach work limit, then we're done with
	 * this round of polling
	 */
	if (rx_work) {
/*如果仍然有数据达到,那么返回POLL方法循环的开始,继续接收数据*/
		if (cpr16(IntrStatus) & cp_rx_intr_mask)
			goto rx_status_loop;
/*这里表示数据已经接收完毕,而且没有新的接收中断产生了,这个时候使能NIC的接收中断,
并且调用__netif_rx_complete把已经完成POLL的设备从poll_list上摘除,等待下一次中断产生的时候,
再次把设备挂上poll_list队列中。*/
		local_irq_disable();
		cpw16_f(IntrMask, cp_intr_mask);
		__netif_rx_complete(dev);
		local_irq_enable();
		return 0;	/* done */
	}
	return 1;		/* not done */
}

其他的使用 NAPI 的驱动程序和 8139CP 大同小异,只是使用了网络层专门提供的 POLL 方法--proecess_backlog(/net/dev.c),在 NIC 中断接收到了数据包后,调用网络层上的 netif_rx(/net/dev.c)将硬件中断中接收到数据帧存入 sk_buff 结构, 然后检查硬件帧头,识别帧类型, 放入接收队列(softnet_data 结构中的 input_pkt_queue 队列上), 激活接收软中断作进一步处理. 软中断函数(net_rx_action)提取接收包,而 process_backlog(也就是 POLL 方法)向上层提交数据。


我们现在来思考一下如何提高 NAPI 效率的问题,在说到效率这个问题之前我们先看一下在linux 的文档中 NAPI_HOWTO.txt 中提供一个模型用来构造自己 NIC 的 POLL 方法,不过和 8139 有一些不一样,其中 NIC 设备描述中有一个 dirty_rx 字段是在 8139CP 中没有使用到的。

dirty_rx 就是已经开辟了 sk_buff 缓冲区指针和已经提交到 NIC 的 rx_ring 参与接收的缓冲,但是还没有完成传输的缓冲区和已经完成传输的缓冲区的数量总和,与之相类似的是 cur_rx 这个表示的是下一个参与传输的缓冲区指针,我们在 NAPI_HOWTO.txt 的举例中可以看到这个字段的一些具体使用方式:


NAPI In Linux Network Device Driver_第3张图片
	/*cur_rx为下一个需要参与传输的缓冲区指针,
	如果cur_rx指针大于dirty_rx那么表示已经有在rx-ring中开辟的rx-ring中的每个传输缓冲已经被耗尽了,
	这个时候需要调用refill_rx_ring 把一些已经向网络层提交了数据的rx-ring接收单元开辟新的缓冲区,
	增加dirty_rx的数值,为下一次数据接收做准备,*/
        if (tp->cur_rx - tp->dirty_rx > RX_RING_SIZE/2 ||
            tp->rx_buffers[tp->dirty_rx % RX_RING_SIZE].skb == NULL) 
                refill_rx_ring(dev);
/*如果已经当前的cur_rx和dirty_rx之间相差不超过总的rx_ring接收单元的一半,
而且剩下的一半中间有空的传输单元,那么我们不必担心了,因为还有足够的缓冲区可以使用(凭经验推断的),
就可以退出当前的程序,等待下一次软中断调用POLL来处理在这之间收到的数据,
(NAPI_HOWTO.txt中是重新启动时钟计数,这样做是在没有使能NIC中断处理的情况下)*/
        if (tp->rx_buffers[tp->dirty_rx % RX_RING_SIZE].skb == NULL)
                restart_timer();
/*如果执行到这里了,那表示有几种情况可能发生,第一当前的cur_rx和dirty_rx之间相差不超过总的rx_ring接收单元的一半,
调用refill_rx_ring后dirty_rx并未增加,(也许在rx-ring中大量的单元收到数据没有得到网络层函数的处理),
结果dirty_rx没有增加,而且也没有空闲的单元来接收新到的数据,这样就要重新调用netif_rx_schedule 来唤醒软中断,
调用设备的POLL方法,采集在rx-ring的数据。*/
	else netif_rx_schedule(dev);  /* we are back on the poll list */
	

在 RTL-8169 的驱动程序中就使用了 dirty_rx 这个字段,但是在 8139CP 中并未使用,其实这个并非 8139CP 驱动不成熟的表现,大家阅读 NAPI_HOWTO.txt 中可以知道,现在 8139CP 中并未严格按照 NAPI 所提出的要求去做,如果大家有兴趣的话,可以比较一下 8139CP 和 RTL-8169 这两个驱动之间的不同,大家会发现虽然两者都没有在 NIC 中断处理中去完成数据从驱动层向网络层上的转发,而都放在了软中断之中完成,但是在 8139 中利用了自己的一些独特的硬件特性,使 NIC 在利用关断中断接收数据的同时利用数据包到达位(RxOK)通知到达事件,然后采用 POLL 方法把数据从 NIC 直接转发到上层;而 RTL8169 还需要借助 softnet_data 结构中的 input_pkt_queue(套接字缓冲(sk_buff)输入队列)来完成从 NIC 中断到软中断之间的 sk_buff 数据调度;这样对于 8139CP 来说最大的好处就是不要了 dirty_rx 字段和 cur_rx 字段来让 POLL 方法以及 NIC 中断知道当前的传输单元的状况,还包括不需要不时定期的调用 refill_rx_ring 来刷新 rx-ring 获得空闲的传输单元;说到坏处我想就是重写了一个 POLL 方法,完全不能借用 /net/core/dev.c 中的 process_backlog 来作为自己的POLL方法,不过这个代价值得。

说了这么多,似乎都和提高效率没有关系,其实正好相反,通过这些了解我们对 softnet_data中的一些字段的意思应该更加清晰了,下面所叙述的,提高效率的方法就是在 8139CP 的基础上借用了 NAPI_ HOWTO.txt 中的一些方法,从实际上的使用效果来看,在某些应用场合之下比 Linux的 8139CP 的确是有了一定的提高,我们首先看看在 Linux2.6.6 的内核使用 8139CP 在x86(PIII-900Mhz)平台上的数据包接收处理情况:比较表如下:

Psize    Ipps       Tput     Rxint            Done
----------------------------------------------------
60     490000      254560      21              10
128     358750      259946      27              11
256     334454      450034      34              18
512     234550      556670      201239          193455
1024    119061      995645      884526          882300
1440     74568      995645      995645          987154

上表中表示:

"Pszie"表示包的大小
"Ipps" 每秒钟系统可以接收的包数量
"Tput" 每次POLL超过 1M 个数据包的总量
"Rxint" 接收中断数量
"Done" 载入 rx-ring 内数据所需要的 POLL 次数,这个数值也表示了我们需要清除 rx-ring 的次数。

从上表可以看出,8139CP 中当接收速率达到 490K packets/s 的时候仅仅只有 21 个中断产生,只需要 10 次 POLL 就可以完成数据从 rx_ring 的接收,然而对于大数据包低速率的情况,接收中断就会急剧增加,直到最后每个数据包都需要一次 POLL 的方法来进行处理,最后的结果就是每个中断都需要一次 POLL 的方法,最后造成效率的急剧下降,以至于系统的效率就会大大降低,所以 NAPI 适用于大量的数据包而且尽可能是小的数据包,但是对于大的数据包,而且低速率的,反而会造成系统速度的下降。

如果要改善这种情况,我们可以考虑采用以下的方法,我们在 MIPS,Xsacle 和 SA1100 平台上进行一系列的测试取得了良好的效果:

1. 完全取消 NIC 中断,使用 RXOK 位置控制接收中断,

2. 采用定时器中断 timer_list 的控制句柄,根据硬件平台设定一个合适的间隔周期(间隔周期依据平台不同而异),对 rx-ring 直接进行 POLL 轮询,我们在 MIPS 和 Xscale 上直接使用了中断向量 0--irq0 作为对 rx-ring 进行轮询的 top-half(注意我们在上述两个平台上选择的 HZ 数值是 1000,而通常这个数值是 100,并且重新编写了 Wall-time 的记数程序,让 Wall-Time 的记数还是以 10MS 为间隔),当然也可以根据自己的平台和应用程序的状况选择合适的定时时间。

3. 借助 softnet_data 中的 input_pkt_queue 队列,在时钟中断 bottom-half 中完成 POLL 方法之后,并不直接把数据传输到网络层进行处理,而是把 sk_buff 挂在 input_pkt_queue队列上,唤醒软中断在过后处理,当然可以想象,这样需要付出一定的内存代价,而且实时性能也要差一些。

4. 使用 dirty_rx 字段和 refill_rx_ring 函数,在调用完 POLL 方法以后,而且网络层程序比较空闲的时候为一些 rx-ring 中的单元建立新缓冲挂在环形缓冲队列上,这样可以在新的数据包达到的时候节省时间,操作系统不必要手忙脚乱地开辟新的空间去应付新来的数据。

5. 最后请注意:我们上层的应用程序是以网络数据转发为主的,并没有在应用层面上有很多后台进程的复杂的应用,上述的 1 到 4 点中所做的显而易见是以牺牲系统效率整体效率而单独改善了网络数据的处理。

我们再来看改善的 8139CP 驱动程序使用 8139CP 在 x86(PIII-900Mhz) 平台上的接收情况:

Psize    Ipps       Tput     Rxint       Done
----------------------------------------------------
60     553500    354560   17         7
128     453000    350400   19         10
256     390050    324500   28         13
512     305600    456670   203        455
1024     123440    340020   772951     123005
1440     64568     344567   822394     130000

从上图来看,数据传输的效率和波动性有很明显的改善,在高速率和低速率的时候所需要的POLL 次数的差距以前的 8139CP 驱动程序那么显著了,这样的而且最大的包接收能力也提高到了 553K/s,我们在 MIPS 系列以及 Xscale 系列平台上最大的包接收能力可以提高大约 15%-25%。

最后使用 NAPI 并不是改善网络效率的唯一途径,只能算是权益之策,根本的解决途径还是在于上层应用程序能够独占网络设备,或者是提供大量的缓冲资源,如果这样,根据我们的实验数据表明可以提高 100%-150% 以上的接收效率。


你可能感兴趣的:(NAPI In Linux Network Device Driver)