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 隊列處於工作狀態的時候是不能把指定設備清除的,否則將會出錯。


何在8139CP使用NAPI:

從 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 的舉例中可以看到這個字段的一些具體使用方式:



	/*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% 以上的接收效率。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章