Linux內(nèi)核源碼分析--詳談NAPI原理機制(超詳細)
1. 引入問題
內(nèi)核收包主要有兩種手段:輪詢和中斷。
通過輪詢,內(nèi)核可以不斷持續(xù)的檢查設(shè)備時候有包收上來,例如設(shè)置一個定時器,定期檢查設(shè)備上的某個定時器。這種方法會輕易浪費掉很多系統(tǒng)資源。
如果采用中斷收包,當(dāng)設(shè)備收到包時,可以產(chǎn)生一個硬件中斷通知內(nèi)核,內(nèi)核將中斷其他活動,然后調(diào)用一個中斷處理程序以滿足設(shè)備的需求,內(nèi)核只是將數(shù)據(jù)包放到某個隊列中并通知內(nèi)核中的收包模塊。這種方式是非常常見的,在低流量負載下是很好的選擇,但是在高流量負載下就無法良好的運行,每接收一個幀就產(chǎn)生一個中斷,很快就會讓CPU為處理中斷而浪費所有的時間。
以太網(wǎng)驅(qū)動收包就是通過以太網(wǎng)設(shè)備產(chǎn)生收包中斷通知內(nèi)核來收包的,但是如上所述,不能每收一個幀都要產(chǎn)生一個中斷,下面的內(nèi)容將介紹驅(qū)動中如何結(jié)合論需和中斷來收包。
2. 幾個關(guān)鍵函數(shù)
有必要先介紹幾個收包相關(guān)的函數(shù),也許會對理解后面的內(nèi)容有幫助。
2.1 netif_receive_skb()
該函數(shù)是內(nèi)核收包的入口,驅(qū)動收到的數(shù)據(jù)包通過這個函數(shù)進入內(nèi)核協(xié)議棧進行處理,我在這里不會分析它的實現(xiàn),只要記住,接下來的幾種驅(qū)動收包方式最終都是為了將數(shù)據(jù)包送到這個函數(shù)。
2.2 net_rx_action()
收包軟中斷處理函數(shù),即中斷下半部。中斷處理函數(shù)要求盡可能快的執(zhí)行完成,內(nèi)核為了快速響應(yīng)中斷,在處理硬件中斷時,只是將數(shù)據(jù)包放到CPU的某個隊列中去,并調(diào)度軟中斷。而實際的數(shù)據(jù)包處理過程則交給中斷下半部處理。
中斷下半部的處理可以通過軟中斷或tasklet來完成:
1.軟中斷:內(nèi)核中定義好了一個收包軟中斷處理函數(shù)net_rx_action(),后面會分析該函數(shù)。
open_softirq(NET_RX_SOFTIRQ,net_rx_action);
2.tasklet:使用tasklet_init(t, func, data)注冊你自己的下半部收包函數(shù)。
工作隊列也可以實現(xiàn)延期執(zhí)行一個函數(shù),但網(wǎng)絡(luò)代碼中主要使用的是軟中斷和tasklet,所以我們不考慮工作隊列。
2.3 dma_alloc_coherent()
函數(shù)原型為:
該函數(shù)用于分配一個DMA一致性緩沖區(qū)。大多數(shù)以太網(wǎng)設(shè)備都支持DMA機制,設(shè)備收到數(shù)據(jù)包后,DMA將其放入內(nèi)存中,并產(chǎn)生一個收包中斷通知CPU從內(nèi)存中拿走數(shù)據(jù)包。
DMA只能識別物理地址,而OS是操作虛擬地址的,這就需要緩存數(shù)據(jù)包的區(qū)域可以讓DMA和OS都能操作。
dma_alloc_coherent()函數(shù)就是為了達到這個目標(biāo),它分配size大小的一致性內(nèi)存,其物理起始地址存放在
dma_handle中,函數(shù)返回值為這段內(nèi)存的虛擬起始地址,這樣,設(shè)備向這塊地址放數(shù)據(jù)包,OS響應(yīng)中斷后可以從這塊地址拿包。
而由于我們要分配一致性內(nèi)存(任何時候,cache中的內(nèi)容和內(nèi)存中的內(nèi)容是相同的),所以返回的虛擬地址盡量是非緩存的,例如在mips中這個虛擬地址就是KSEG1中的地址。
3. 舊的收包接口netif_rx
在設(shè)備驅(qū)動在DMA中拿到一個數(shù)據(jù)包,做一些和設(shè)備相關(guān)的處理后,初始化一個skb實例,就可以將數(shù)據(jù)包交給netif_rx()來處理了。
內(nèi)核中定義了全局的per-cpu收包隊列softnet_data,其定義如下:
結(jié)構(gòu)體struct softnet_data的定義:
netif_rx()函數(shù)中主要是調(diào)用enqueue_to_backlog()將skb放入per-cpu的收包隊列中去。
函數(shù)流程比較清晰:
獲得per-cpu的softnet_data結(jié)構(gòu)實例sd。
如果sd->input_pkt_queue收包隊列長度超出限制,即收到的包積滿了,直接給sd->dropped++,并嘗試釋放skb,返回NET_RX_DROP。這個限制的默認值為1000,可在/proc/sys/net/core/netdev_max_backlog中查看和修改。
如果sd->input_pkt_queue收包隊列長度未達到限制,則將skb放入sd->input_pkt_queue隊列中。這里分兩種情況:
如果sd->input_pkt_queue是空的,則將napi設(shè)備sd->backlog添加到poll_list輪詢列表中去,并調(diào)度NET_RX_SOFTIRQ,同時將napi設(shè)備sd->backlog的state設(shè)置為NAPI_STATE_SCHED。然后將skb加入到sd->input_pkt_queue的隊尾。
如果sd->input_pkt_queue不是空的,則NET_RX_SOFTIRQ已經(jīng)被調(diào)度過了,所以,直接將skb加入到sd->input_pkt_queue的隊尾即可。
這里需要注意兩點:
1. 把幀排入隊列是相當(dāng)快的,因為不涉及任何內(nèi)存拷貝,只是指針操作而已。
2. 在操作per-cpu變量softnet_data時,需要關(guān)閉本地中斷(netif_rx可能不是在中斷處理程序中被調(diào)用的,所以此時本地中斷可能是開啟的)。
___napi_schedule()函數(shù)用于將napi設(shè)備添加到poll_list輪詢列表中,并調(diào)度NET_RX_SOFTIRQ。
早在dev module初始化的時候,net_dev_init()中就定義了softnet_data中napi結(jié)構(gòu)的poll函數(shù)以及軟中斷處理函數(shù):
調(diào)度了軟中斷,則后續(xù)會執(zhí)行下半部函數(shù)net_rx_action()。
net_rx_action()是一個很重要的下半部收包函數(shù),NAPI設(shè)備和非NAPI設(shè)備都可能會使用它來收包。該函數(shù)的主要工作就是操作收包隊列和執(zhí)行poll函數(shù)。
該函數(shù)的流程如下:
遍歷softnet_data 的輪詢列表sd->poll_list,并取出其中的napi struct,獲得napi設(shè)備的weight和poll函數(shù)。
如果napi設(shè)備的狀態(tài)為被調(diào)度(NAPI_STATE_SCHED),則調(diào)用poll函數(shù)進行實際的收包,poll函數(shù)返回實際收包個數(shù),根據(jù)這個返回值,會有不同的動作:
如果收包個數(shù)小于weight,說明收包已經(jīng)完成,則將該napi設(shè)備從輪詢列表中刪除(在poll函數(shù)中完成,所以這里代碼中看不到),然后繼續(xù)遍歷softnet_data 的輪詢列表。
如果收包個數(shù)等于weight,則可能還有數(shù)據(jù)包沒收完,則將該napi設(shè)備移到輪詢列表的末尾,使之后續(xù)還能遍歷到。然后繼續(xù)遍歷softnet_data 的輪詢列表。
將napi設(shè)備從輪詢列表中刪除是在函數(shù)napi_complete()中完成的,它除了從softnet_data的輪詢列表sd->poll_list中刪除napi設(shè)備,還將該設(shè)備的state的NAPI_STATE_SCHED位清除。
napi的state有三種:
NAPI_STATE_SCHED:napi設(shè)備是否被調(diào)度了,1:被調(diào)度了,0:沒有被調(diào)度。
NAPI_STATE_DISABLE:napi設(shè)備是否被屏蔽了,如果被屏蔽,則不能被調(diào)度。
NAPI_STATE_NPSVC:netpoll機制中使用,我們不關(guān)注。
在net_rx_action()函數(shù)中還對每一次軟中斷處理的時間做了限制,這是由兩個變量來控制的:
time_limit = jiffies + 2,如果當(dāng)前時間超過了time_limit,就強制終止此次軟中斷處理。即時間不能超過2個jiffies。
budget = netdev_budget,每次poll函數(shù)返回,budget就減去此次收包數(shù),當(dāng)budget減到0時,就強制終止此次軟中斷處理。netdev_budget設(shè)置的值為300。
強制終止此次軟中斷處理并不是不處理了,這是為了與其他任務(wù)公平運行,net_rx_action會主動釋放CPU,當(dāng)然softnet_data中很可能還有沒輪詢到的napi設(shè)備,所以,net_rx_action()重新調(diào)度NET_RX_SOFTIRQ軟中斷,讓內(nèi)核后面有時間再進行處理。
另外需要注意的是,在操作softnet_data的時候需要關(guān)閉本地中斷,而在進行軟中斷處理時,是開中斷的。 poll函數(shù)用于實際的內(nèi)核收包,在不使用NAPI機制時,softnet_data的poll函數(shù)固定為process_backlog(),他接受兩個參數(shù):napi實例和weight(即下面函數(shù)參數(shù)中的quota)。
該函數(shù)就是從input_pkt_queue隊列上拿包,然后交給__netif_receive_skb()處理,即我們最開始說的協(xié)議棧收包入口。當(dāng)收包數(shù)量超過napi設(shè)置的weight,就結(jié)束該函數(shù)并返回收包數(shù)。
在處理收包隊列時,實際上每次操作的都是process_queue隊列,input_pkt_queue隊列上的包也是放到process_queue隊列中再處理的。process_queue隊列不知道什么時候加到內(nèi)核中去的,好像是為了收取offline CPU的數(shù)據(jù)包。
至此,__netif_receive_skb()收到包了,我們講netif_rx函數(shù)就先告一段落。接下來看看NAPI機制下的收包流程有什么不同。
4. NAPI機制
上面講到的netif_rx函數(shù)在收包過程中已經(jīng)用到了napi_strcut結(jié)構(gòu),因為軟中斷處理使用了NAPI的框架,本章講述NAPI機制的工作流程,你會發(fā)現(xiàn),軟中斷處理過程和上面講到的沒什么差別。
對了,NAPI是New API的縮寫,即處理入口幀的一套新API,雖然這個名字沒什么可擴展性,但是NAPI估計能撐很長時間,所以在更new的API出來之前,暫時不用考慮給它換名字。
NAPI機制采用中斷和輪詢結(jié)合的方式收包,防止收包中斷太多處理不過來。傳統(tǒng)的API是每收到一個包就產(chǎn)生一個中斷,在與高速網(wǎng)絡(luò)適配器協(xié)作時,就會遇到在處理一個中斷時另一個中斷已經(jīng)來了,而處理中斷過程是關(guān)中斷的,那新的中斷就會被阻塞。
NAPI使用了IRQ和輪詢的組合。假設(shè)數(shù)據(jù)分組將以高頻率頻繁到達,NAPI的工作機制如下:
第一個分組將導(dǎo)致網(wǎng)絡(luò)適配器發(fā)出IRQ,為防止進一步的分組導(dǎo)致更多的IRQ,驅(qū)動程序會關(guān)閉該適配器的rx IRQ,并將該適配器放到一個輪詢表上。
只要適配器上還有分組需要處理,內(nèi)核就一直對輪詢表上的設(shè)備進行輪詢,處理剩下的分組。
重新啟動rx IRQ。
如果在新分組到達時,舊的分組仍然處于處理過程中,工作也不會因額外的中斷而減速。
只有設(shè)備滿足如下兩個條件,才能實現(xiàn)NAPI方法:
設(shè)備必須能夠保留多個接收的分組,例如保存到DMA環(huán)形緩沖區(qū)中。
設(shè)備必須能夠禁止用于接收分組的IRQ,而且發(fā)送分組或其他可能通過IRQ進行的操作,都仍然必須是啟用的。
幾乎所有的網(wǎng)卡都是支持DMA模式的,能夠自行將數(shù)據(jù)傳輸?shù)轿锢韮?nèi)存并通知CPU處理。 初始化一個napi實例的函數(shù)為netif_napi_add(),就是給napi_struct做初始化工作:
該函數(shù)接受四個參數(shù):
dev:napi實例所屬的設(shè)備。
napi:將要做初始化的napi實例。
poll:napi的poll函數(shù)。支持NAPI必須提供一個poll函數(shù)。
weight:napi的權(quán)重值??梢匀∪魏沃担荒艹^該設(shè)備可以在rx緩沖區(qū)中存儲的分組的數(shù)目,通常10/100Mbit網(wǎng)卡驅(qū)動指定為16,而1000/10000Mbit網(wǎng)卡驅(qū)動指定為64。
做了初始化的成員我們上面都講到過了,在此不贅述。netif_napi_add()函數(shù)的目的就是完成一個napi_struct結(jié)構(gòu)實例的初始化,后續(xù)將被添加到輪詢列表中,通常在網(wǎng)卡驅(qū)動的xxx_probe()階段被調(diào)用。
下面我們從設(shè)備驅(qū)動的收包開始談NAPI的工作機制:
在設(shè)備收包一個包時,驅(qū)動中注冊的收包中斷處理函數(shù)被執(zhí)行,中斷處理函數(shù)中不同做太多事情,實際上只需要做兩件事:
關(guān)閉設(shè)備的收包中斷。
調(diào)用napi_schedule(napi)函數(shù)將我們初始化好的napi對象注冊到輪詢列表中,并調(diào)度軟中斷。
關(guān)閉設(shè)備中斷后,設(shè)備收到包后不再產(chǎn)生中斷(或者內(nèi)核不再響應(yīng)中斷),而只是將數(shù)據(jù)包放到DMA中。
napi_schedule()的實現(xiàn)如下:
napi_schedule_prep()先做一些檢查工作:如果napi對象的狀態(tài)為NAPI_STATE_DISABLE或已經(jīng)是NAPI_STATE_SCHED,則不進行調(diào)度,如果沒有設(shè)置NAPI_STATE_SCHED標(biāo)記,則置上NAPI_STATE_SCHED標(biāo)記。
接下來__napi_schedule()就是添加到當(dāng)前CPU的softnet_data結(jié)構(gòu)的poll_list輪詢列表中(這里由于操作了softnet_data,因此要關(guān)一下中斷),并調(diào)度NET_RX_SOFTIRQ軟中斷(對應(yīng)前面,開中斷)。
調(diào)度NET_RX_SOFTIRQ軟中斷后,內(nèi)核后續(xù)會去執(zhí)行處理函數(shù)net_rx_action()。這個函數(shù)的流程我們已經(jīng)講過,和前面唯一的不同就在于poll函數(shù),在非NAPI收包過程中,poll函數(shù)是在netif_rx()中注冊的process_backlog()函數(shù),而NAPI收包中的poll函數(shù)是我們在netif_napi_add()中注冊的,即自定義的一個函數(shù)。
接下來就看一下一個實際的NAPI設(shè)備收包的poll函數(shù)都是怎么實現(xiàn)的,我不帖特定的代碼,只是大致說一下流程:
進行收包,這里收包并不是從softnet_data的某個隊列收包,由于CPU已經(jīng)不接受設(shè)備的收包中斷了,所以在DMA中可能會積壓了一些包,所以直接從DMA的緩沖區(qū)中收包,并交給netif_receive_skb()進入?yún)f(xié)議棧。每次收包的數(shù)量由napi對象的weight權(quán)值限制。
如果收包個數(shù)小于weight的值,說明全收完了。則調(diào)用napi_complete()將napi對象從輪詢列表中刪除,并清除其NAPI_STATE_SCHED位,同時開啟設(shè)備的收包中斷,返回收包個數(shù)到net_rx_action。
如果收包個數(shù)大于等于weight的值,說明可能還沒收完,則返回收包個數(shù)到net_rx_action。 也就是說,在關(guān)閉收包中斷的情況下,napi的poll函數(shù)會去不停的從DMA中收包,直到收完才開中斷,開中斷后的下一個包就以中斷的方式通知CPU收包。當(dāng)然中斷不能長時間關(guān)閉,前面講到在net_rx_action設(shè)置了每次軟中斷的時間限制。
講到這里我們需要對比一下直接使用netif_rx收包和使用NAPI收包的區(qū)別:

netif_rx收包

NAPI收包
從圖中獲取數(shù)據(jù)包的方式就可以看出NAPI相對于單純的netif_rx的優(yōu)勢。為什么說單純的netif_rx呢。因為,目前還有很多不使用NAPI收包的設(shè)備驅(qū)動,這些驅(qū)動可以采用其他類似NAPI的方法,來緩解高吞吐量下的中斷風(fēng)暴。
5. 在中斷期間處理多幀
一些驅(qū)動雖然沒有使用NAPI收包機制,但在驅(qū)動中通過設(shè)置類似weight的權(quán)值,實現(xiàn)在一個中斷到來時嘗試處理多個數(shù)據(jù)包。
例如,有些驅(qū)動在中斷處理程序中添加了一個quota值,限定每次中斷可以處理數(shù)據(jù)包的個數(shù),在每次中斷到來時關(guān)閉設(shè)備自身的收包中斷,并嘗試從DMA中獲取不大于quota數(shù)量的數(shù)據(jù)包,每次獲取到數(shù)據(jù)包就交給netif_rx處理或直接交給netif_receive_skb()。當(dāng)然,拿包并處理的過程可能比較長,那么可以將這些動作放到tasklet任務(wù)中,中斷處理程序只需調(diào)度tasklet任務(wù)即可。在處理完quota個數(shù)據(jù)包之后再開啟設(shè)備的收包中斷。
這樣一來,使用quota結(jié)合netif_rx,就實現(xiàn)了在一次中斷中處理多個包。
