原來 Linux 是這么接收網(wǎng)絡(luò)幀的

本文將從初學(xué)者角度,介紹 Linux 內(nèi)核如何接收網(wǎng)絡(luò)幀:從網(wǎng)卡設(shè)備完成數(shù)據(jù)幀的接收開始,到數(shù)據(jù)幀被傳遞到網(wǎng)絡(luò)棧中的第三層結(jié)束。著重介紹內(nèi)核的工作機制,不會深入過多驅(qū)動層面的細節(jié),示例代碼來自 Linux 2.6。
設(shè)備的通知手段
當網(wǎng)絡(luò)設(shè)備接收到數(shù)據(jù),并儲存到設(shè)備的接收幀緩沖區(qū)后(該緩沖區(qū)可能位于設(shè)備的內(nèi)存,也可能通過 DMA 寫入到主機內(nèi)存的接收環(huán)),必須通知內(nèi)核對已接收的數(shù)據(jù)進行處理。
輪詢
輪詢(Polling)指的是內(nèi)核主動地去檢查設(shè)備,比如定期讀取設(shè)備的內(nèi)存寄存器,判斷是否有新的接收幀需要處理。這種方式在設(shè)備負載較高時響應(yīng)效率低,在設(shè)備負載低時又占用系統(tǒng)資源,操作系統(tǒng)很少單獨采用,結(jié)合其他機制后才能實現(xiàn)較理想的效果。
硬件中斷
當接收到新的數(shù)據(jù)幀等事件發(fā)生時,設(shè)備將生成一個硬件中斷信號。該信號通常由設(shè)備發(fā)送給中斷控制器,由中斷控制器轉(zhuǎn)發(fā)給 CPU。CPU 接受信號后將從當前執(zhí)行的任務(wù)中被打斷,轉(zhuǎn)而執(zhí)行由設(shè)備驅(qū)動注冊的中斷處理程序來處理設(shè)備事件。中斷處理程序會將數(shù)據(jù)幀加入到內(nèi)核的輸入隊列中,并通知內(nèi)核做進一步處理。這種技術(shù)在低負載時表現(xiàn)良好,因為每一個數(shù)據(jù)幀都會得到及時響應(yīng),但在負載較高時,CPU 會被頻繁的打斷從而影響到其他任務(wù)的執(zhí)行。
對接收幀的處理通常分為兩個部分:首先驅(qū)動注冊的中斷處理程序?qū)瑥?fù)制到內(nèi)核可訪問的輸入隊列中,然后內(nèi)核對其進行處理,通常是將其傳遞給相關(guān)協(xié)議的處理程序,如 IPv4。第一部分的中斷處理程序是在中斷上下文中執(zhí)行的,可以搶占第二部分的執(zhí)行,這意味著幀復(fù)制接收幀到輸入隊列的程序比消費數(shù)據(jù)幀的協(xié)議棧程序有更高的優(yōu)先級。
在高流量負載下,中斷處理程序會不斷搶占 CPU。后果顯而易見:輸入隊列最終將被填滿,但應(yīng)該去出隊并處理這些幀的程序處于較低優(yōu)先級沒有機會執(zhí)行。結(jié)果新的接收幀因為輸入隊列已滿無法加入隊列,而舊的幀因為沒有可用的 CPU 資源不會被處理。這種情況被稱為接收活鎖(receive-livelock)。
硬件中斷的優(yōu)點是幀的接收和處理之間的延遲非常低,但在高負載下會嚴重影響其他內(nèi)核或用戶程序的執(zhí)行。大多數(shù)網(wǎng)絡(luò)驅(qū)動會使用硬件中斷的某種優(yōu)化版本。
一次處理多個幀
一些設(shè)備驅(qū)動會采用一種改良方式,當中斷處理程序被執(zhí)行時,它會在指定的窗口時間或幀數(shù)量上限內(nèi)持續(xù)地入隊數(shù)據(jù)幀。由于中斷處理程序執(zhí)行時其他中斷將被禁用,因此必須設(shè)置合理的執(zhí)行策略來和其他任務(wù)共享 CPU 資源。
該方式還可進一步優(yōu)化,設(shè)備僅通過硬件中斷來通知內(nèi)核有待處理的接收幀,將入隊并處理接收幀的工作交給內(nèi)核的其他處理程序來執(zhí)行。這也是 Linux 的新接口 NAPI 的工作方式。
計時中斷
除了根據(jù)事件立刻生成中斷,設(shè)備也可在有接收幀時,以固定的間隔發(fā)送中斷,中斷處理程序?qū)z查這段間隔時間內(nèi)是否有新的幀,并一次性處理它們。如果所有接收幀已經(jīng)處理完畢并且沒有新的幀,設(shè)備會停止發(fā)送中斷。
這種方式要求設(shè)備在硬件層面實現(xiàn)計時功能,而且根據(jù)計時間隔長短會帶來固定的處理延遲,但在高負載時可以有效地減少 CPU 占用并且避免接收活鎖。
在實踐中的組合
不同的通知機制有其適合的工作場景:低負載下純中斷模型保證了極低延遲,但在高負載下表現(xiàn)糟糕;計時中斷在低負載下可能會引入過高延遲并浪費 CPU 時間,但在高負載下對減少 CPU 占用和解決接收活鎖有很大幫助。在實踐中,網(wǎng)絡(luò)設(shè)備往往不依賴某種單一模型,而是采取組合方案。
以 Linux 2.6 Vortex 設(shè)備所注冊的中斷處理函數(shù) vortex_interrupt (位于 /drivers/net/3c59x.c)為例:
設(shè)備會將多個事件歸類為一種中斷類型(甚至還可以在發(fā)送中斷信號前等待一段時間,將多個中斷聚合成一個信號發(fā)送)。中斷觸發(fā) vortex_interrupt的執(zhí)行并禁用該 CPU 上的中斷。如果中斷是由接收幀事件 RxComplete引發(fā),處理程序調(diào)用其他代碼處理設(shè)備接收的幀。vortex_interrupt在執(zhí)行期間持續(xù)讀取設(shè)備寄存器,檢查是否有新的中斷信號發(fā)出。如果有且中斷事件為RxComplete,處理程序?qū)⒗^續(xù)處理接收幀,直到已處理幀的數(shù)量達到預(yù)設(shè)的work_done值才結(jié)束。而其他類型的中斷將被處理程序忽略。
軟中斷處理機制
一個中斷通常會觸發(fā)以下事件:
設(shè)備產(chǎn)生一個中斷并通過硬件通知內(nèi)核。 如果內(nèi)核沒有正在處理另一個中斷(即中斷沒有被禁用),它將收到這個通知。 內(nèi)核禁用本地 CPU 的中斷,并執(zhí)行與收到的中斷類型相關(guān)聯(lián)的處理程序。 內(nèi)核退出中斷處理程序,重新啟用本地 CPU 的中斷。
CPU 收到中斷通知時會調(diào)用與該中斷號對應(yīng)的處理程序,在處理程序的執(zhí)行期間內(nèi)核代碼處于中斷上下文,中斷會被禁用。這意味著 CPU 在處理某個中斷期間,它既不會處理其他中斷,也不能被其他進程搶占,CPU 資源由該中斷處理程序獨占。這種設(shè)計決定減少了競爭條件的可能性,但也帶來了潛在的性能影響。
顯然,中斷處理程序應(yīng)當盡可能快地完成工作。不同的中斷事件所需要的處理工作量并不相同,比如當鍵盤的按鍵被按下時,觸發(fā)的中斷處理函數(shù)只需要將該按鍵的編碼記錄下來,而且這種事件的發(fā)生頻率不會很高;而處理網(wǎng)絡(luò)設(shè)備收到的新數(shù)據(jù)幀時,需要為 skb 分配內(nèi)存空間,拷貝接收到的數(shù)據(jù),同時完成一些初始化工作比如判斷數(shù)據(jù)所屬的網(wǎng)絡(luò)協(xié)議等。
為此操作系統(tǒng)為中斷處理程序引入了上、下半部的概念。
下半部處理程序
即使由中斷觸發(fā)的處理動作需要大量的 CPU 時間,大部分動作通常是可以等待的。中斷可以第一時間搶占 CPU 執(zhí)行,因為如果操作系統(tǒng)讓硬件等待太長時間,硬件可能會丟失數(shù)據(jù)。這既適用于實時的數(shù)據(jù),也適用于在固定大小緩沖區(qū)中存儲的數(shù)據(jù)。如果硬件丟失了數(shù)據(jù),一般沒有辦法再恢復(fù)(不考慮發(fā)送方重傳的情況)。另一方面,內(nèi)核或用戶空間的進程被推遲執(zhí)行或搶占時,一般不會有什么損失(對實時性有極高要求的系統(tǒng)除外,它需要用完全不同的方式來處理進程和中斷)。
鑒于這些考慮,現(xiàn)代中斷處理程序被分為上半部和下半部。上半部分執(zhí)行在釋放 CPU 資源之前必須完成的工作,如保存接收的數(shù)據(jù);下半部分則執(zhí)行可以在推遲到空閑時完成的工作,如完成接收數(shù)據(jù)的進一步處理。
你可以認為下半部是一個可以異步執(zhí)行的特定函數(shù)。當一個中斷觸發(fā)時,有些工作并不要求馬上完成,我們可以把這部分工作包裝為下半部處理程序延后執(zhí)行。上、下半部工作模型可以有效縮短 CPU 處于中斷上下文(即禁用中斷)的時間:
設(shè)備向 CPU 發(fā)出中斷信號,通知它有特定事件發(fā)生。 CPU 執(zhí)行中斷相關(guān)的上半部處理函數(shù),禁用之后的中斷通知,直到處理程序完成工作:a. 將一些數(shù)據(jù)保存在內(nèi)存中,用于內(nèi)核在之后進一步處理中斷事件。b. 設(shè)置一個標志位,以確保內(nèi)核知道有待處理的中斷。c. 在終止之前重新啟用本地 CPU 的中斷通知。 在之后的某個時間點,當內(nèi)核沒有更緊迫的任務(wù)處理時,會檢查上半部處理程序設(shè)置的標志位,并調(diào)用關(guān)聯(lián)的下半部分處理程序。調(diào)用之后它會重置這個標志位,進入下一輪處理。
Linux 為下半部處理實現(xiàn)了多種不同的機制:軟中斷、微任務(wù)和工作隊列,這些機制同樣適用于操作系統(tǒng)中的延時任務(wù)。下半部處理機制通常都有以下共同特性:
定義不同的類型,并在類型和具體的處理任務(wù)之間建立關(guān)聯(lián)。 調(diào)度處理任務(wù)的執(zhí)行。 通知內(nèi)核有已調(diào)度的任務(wù)需要執(zhí)行。
接下來著重介紹處理網(wǎng)絡(luò)數(shù)據(jù)幀用到的軟中斷機制。
軟中斷
軟中斷有以下幾種常用類型:
enum
{
?HI_SOFTIRQ=0,
?TIMER_SOFTIRQ,
?NET_TX_SOFTIRQ,
?NET_RX_SOFTIRQ,
?BLOCK_SOFTIRQ,
?IRQ_POLL_SOFTIRQ,
?TASKLET_SOFTIRQ,
};
其中 NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ 用于處理網(wǎng)絡(luò)數(shù)據(jù)的接收和發(fā)送。
調(diào)度與執(zhí)行時機
每次網(wǎng)絡(luò)設(shè)備接收一個幀后,會發(fā)送硬件中斷通知內(nèi)核調(diào)用中斷處理程序,處理程序通過以下函數(shù)在本地 CPU 上觸發(fā)軟中斷的調(diào)度:
__raise_softirq_irqoff:在一個專門的 bitmap (位圖)結(jié)構(gòu)中設(shè)置與軟中斷類型對應(yīng)的比特位,當后續(xù)對該比特位的檢查結(jié)果為真時,調(diào)用與軟中斷關(guān)聯(lián)的處理程序。每個 CPU 使用一個單獨的 bitmap。raise_softirq_irqoff:內(nèi)部包裝了__raise_softirq_irqoff函數(shù)。如果此函數(shù)不是從中斷上下文中調(diào)用,且搶占未被禁用,將會額外調(diào)度一個ksoftirqd線程。raise_softirq: 內(nèi)部包裝了raise_softirq_irqoff,但執(zhí)行時會禁用 CPU 中斷。
在特定的時機,內(nèi)核會檢查每個 CPU 獨有的 bitmap 判斷是否有已調(diào)度的軟中斷等待執(zhí)行,如果有將會調(diào)用 do_softirq 處理軟中斷。內(nèi)核處理軟中斷的時機如下:
do_IRQ
每當內(nèi)核收到一個硬件中斷的 IRQ 通知時,會調(diào)用
do_IRQ來執(zhí)行中斷對應(yīng)的處理程序。中斷處理程序中可能會調(diào)度新的軟中斷,因此在do_IRQ結(jié)束時處理軟中斷是一個很自然的設(shè)計,也可以有效的降低延遲。此外,內(nèi)核的時鐘中斷還保證了兩次軟中斷處理時機之間的最大時間間隔。大部分架構(gòu)的內(nèi)核會在退出中斷上下文步驟
irq_exit()中調(diào)用do_softirq:
unsigned?int?__irq_entry?do_IRQ(struct?pt_regs?*regs)
{
?......
?exit_idle();
?irq_enter();
?//?handle?irq?with?registered?handler
?irq_exit();
?set_irq_regs(old_regs);
?return?1;
}
在 irq_exit() 中,如果內(nèi)核已經(jīng)退出中斷上下文且有待執(zhí)行的軟中斷,將調(diào)用 invoke_softirq():
void?irq_exit(void)
{
?account_system_vtime(current);
?trace_hardirq_exit();
?sub_preempt_count(IRQ_EXIT_OFFSET);
?if?(!in_interrupt()?&&?local_softirq_pending())
?????invoke_softirq();
?rcu_irq_exit();
?preempt_enable_no_resched();
}
invoke_softirq 是對 do_softirq 的簡單封裝:
static?inline?void?invoke_softirq(void)
{
?if?(!force_irqthreads)
?????do_softirq();
?else
?????wakeup_softirqd();
}
從中斷和異常事件(包括系統(tǒng)調(diào)用)返回時,這部分處理邏輯直接寫入了匯編代碼。 調(diào)用 local_bh_enable開啟軟中斷時,將執(zhí)行待處理的軟中斷。每個處理器有一個軟中斷線程 ksoftirqd_CPUn,該線程執(zhí)行時也會處理軟中斷。
軟中斷執(zhí)行時 CPU 中斷是開啟的,軟中斷可以被新的中斷掛起。但如果軟中斷的一個實例已經(jīng)在一個 CPU 上運行或掛起,內(nèi)核將禁止該軟中斷類型的新請求在 CPU 上運行,這樣可以大幅減少軟中斷所需的并發(fā)鎖。
處理軟中斷 do_softirq
當執(zhí)行軟中斷的時機達成,內(nèi)核會執(zhí)行 do_softirq 函數(shù)。
do_softirq 首先會將待執(zhí)行的軟中斷保存一份副本。在 do_ softirq 運行時,同一個軟中斷類型有可能被調(diào)度多次:運行軟中斷處理程序時可以被硬件中斷搶占,處理中斷時期間可以重新設(shè)置 cpu 的待處理軟中斷 bitmap,也就是說,在執(zhí)行一個待處理的軟中斷期間,這個軟中斷可能會被重新調(diào)度。出于這個原因,do_softirq 會首先禁用中斷,將待處理軟中斷的 bitmap 保存一份副本到局部變量 pending 中,然后將本地 CPU 的軟中斷 bitmap 中對應(yīng)的位重置為 0,隨后重新開啟中斷。最后,基于副本 pending 依次檢查每一位是否為 1,如果是則根據(jù)軟中斷類型調(diào)用對應(yīng)的處理程序:
do?{
??if?(pending?&?1)?{
???unsigned?int?vec_nr?=?h?-?softirq_vec;
???int?prev_count?=?preempt_count();
???kstat_incr_softirqs_this_cpu(vec_nr);
???trace_softirq_entry(vec_nr);
???h->action(h);
???trace_softirq_exit(vec_nr);
???if?(unlikely(prev_count?!=?preempt_count()))?{
????printk(KERN_ERR?"huh,?entered?softirq?%u?%s?%p"
???????????"with?preempt_count?%08x,"
???????????"?exited?with?%08x?\n",?vec_nr,
???????????softirq_to_name[vec_nr],?h->action,
???????????prev_count,?preempt_count());
????preempt_count()?=?prev_count;
???}
???rcu_bh_qs(cpu);
??}
??h++;
??pending?>>=?1;
?}?while?(pending);
等待中的軟中斷調(diào)用次序取決于位圖中標志位的位置以及掃描這些標志的方向(由低位到高位),并不是以先進先出的方式執(zhí)行的。
當所有的處理程序執(zhí)行完畢后,do_ softirq 再次禁用中斷,并重新檢查 CPU 的待處理中斷 bitmap,如果發(fā)現(xiàn)又有新的待處理軟中斷,則再次創(chuàng)建一份副本重新執(zhí)行上述流程。這種處理流程最多會重復(fù)執(zhí)行 MAX_SOFTIRQ_RESTART 次(通常值為 10),以避免無限搶占 CPU 資源。
當處理輪次到達 MAX_SOFTIRQ_RESTART 閾值時,do_ softirq 必須結(jié)束執(zhí)行,如果此時依然有未執(zhí)行的軟中斷,將喚醒 ksoftirqd 線程來處理。但是 do_ softirq 在內(nèi)核中的調(diào)用頻率很高,實際上后續(xù)調(diào)用的 do_softirq 可能會在 ksoftirqd 線程被調(diào)度之前就處理完了這些軟中斷。
ksoftirqd 內(nèi)核線程
每個 CPU 都有一個內(nèi)核線程 ksoftirqd(通常根據(jù) CPU 序號命名為 ksoftirqd_CPUn),當上文描述的機制無法處理完所有的軟中斷時,該 CPU 位于后臺的 ksoftirqd 線程被喚醒,并承擔(dān)起在獲得調(diào)度后盡可能多的處理待執(zhí)行軟中斷的職責(zé)。
ksoftirqd 關(guān)聯(lián)的任務(wù)函數(shù) run_ksoftirqd 如下:
static?int?run_ksoftirqd(void?*?__bind_cpu)
{
?set_current_state(TASK_INTERRUPTIBLE);
?while?(!kthread_should_stop())?{
??preempt_disable();
??if?(!local_softirq_pending())?{
???preempt_enable_no_resched();
???schedule();
???preempt_disable();
??}
??__set_current_state(TASK_RUNNING);
??while?(local_softirq_pending())?{
???/*?Preempt?disable?stops?cpu?going?offline.
??????If?already?offline,?we'll?be?on?wrong?CPU:
??????don't?process?*/
???if?(cpu_is_offline((long)__bind_cpu))
????goto?wait_to_die;
???local_irq_disable();
???if?(local_softirq_pending())
????__do_softirq();
???local_irq_enable();
???preempt_enable_no_resched();
???cond_resched();
???preempt_disable();
???rcu_note_context_switch((long)__bind_cpu);
??}
??preempt_enable();
??set_current_state(TASK_INTERRUPTIBLE);
?}
?__set_current_state(TASK_RUNNING);
?return?0;
wait_to_die:
?preempt_enable();
?/*?Wait?for?kthread_stop?*/
?set_current_state(TASK_INTERRUPTIBLE);
?while?(!kthread_should_stop())?{
??schedule();
??set_current_state(TASK_INTERRUPTIBLE);
?}
?__set_current_state(TASK_RUNNING);
?return?0;
}
ksoftirqd 做的事情和 do_softirq 基本相同,其主要邏輯是通過 while 循環(huán)不斷的調(diào)用 __do_softirq (該函數(shù)也是 do_softirq 的核心邏輯),只有達到以下兩種條件時才會停止:
沒有待處理的軟中斷時,此時 ksoftirqd 會調(diào)用 schedule()觸發(fā)調(diào)度主動讓出 CPU 資源。該線程執(zhí)行完畢被分配的時間分片,被要求讓出 CPU 資源等待下一次調(diào)度。
ksoftirqd 線程設(shè)置的調(diào)度優(yōu)先級很低,同樣可以避免軟中斷較多時搶占過多的 CPU 資源。
網(wǎng)絡(luò)幀的接收
Linux 的網(wǎng)絡(luò)系統(tǒng)主要使用以下兩種軟中斷類型:
NET_RX_SOFTIRQ 用于處理接收(入站)網(wǎng)絡(luò)數(shù)據(jù) NET_TX_SOFTIRQ 用于處理發(fā)送(出棧)網(wǎng)絡(luò)數(shù)據(jù)
本文主要聚焦于如何接收數(shù)據(jù)。
輸入隊列
每個 CPU 都有一個存放接收網(wǎng)絡(luò)幀的輸入隊列 input_pkt_queue,這個隊列位于 softnet_data 結(jié)構(gòu)中,但并不是所有的網(wǎng)卡設(shè)備驅(qū)動都會使用這個輸入隊列:
struct?softnet_data?{
?struct?Qdisc??*output_queue;
?struct?Qdisc??**output_queue_tailp;
?struct?list_head?poll_list;
?struct?sk_buff??*completion_queue;
?struct?sk_buff_head?process_queue;
?/*?stats?*/
?unsigned?int??processed;
?unsigned?int??time_squeeze;
?unsigned?int??cpu_collision;
?unsigned?int??received_rps;
?unsigned??dropped;
?struct?sk_buff_head?input_pkt_queue;
?struct?napi_struct?backlog;
};
Linux New API (NAPI)
網(wǎng)卡設(shè)備每接收到一個二層的網(wǎng)絡(luò)幀后,使用硬件中斷來向 CPU 發(fā)出信號,通知其有新的幀需要處理。收到中斷的 CPU 會執(zhí)行 do_IRQ 函數(shù),調(diào)用與硬件中斷號關(guān)聯(lián)的處理程序。處理程序通常是由設(shè)備驅(qū)動程序在初始化時注冊的一個函數(shù),這個中斷處理程序?qū)⒃诮弥袛嗄J较聢?zhí)行,使得 CPU 暫時停止接收中斷信號。中斷處理程序會執(zhí)行一些必要的即時任務(wù),并將其他任務(wù)調(diào)度到下半部中延遲執(zhí)行。具體來說中斷處理程序會做這些事情:
將網(wǎng)絡(luò)幀復(fù)制到 sk_buff數(shù)據(jù)結(jié)構(gòu)中。初始化一些 sk_buff的參數(shù),供上層的網(wǎng)絡(luò)棧使用。特別是skb->protocol,它標識了上層的協(xié)議處理程序。更新其他的設(shè)備專用參數(shù)。 通過調(diào)度軟中斷 NET_RX_SOFTIRQ 來通知內(nèi)核進一步處理接收幀。
我們上文介紹過輪詢和中斷通知機制(包括幾種改良版本),它們有不同的優(yōu)缺點,適用不同的工作場景。Linux 在 Linux 2.6 引入了一種混合了輪詢和中斷的 NAPI 機制來通知并處理新的接收幀。NAPI 在高負載場景下有良好表現(xiàn),還能顯著的節(jié)省 CPU 資源。本文將重點介紹 NAPI 機制。
當設(shè)備驅(qū)動支持 NAPI 時,設(shè)備在接收到網(wǎng)絡(luò)幀后依然使用中斷通知內(nèi)核,但內(nèi)核在開始處理中斷后將禁用來自該設(shè)備的中斷,并持續(xù)地通過輪詢方式從設(shè)備的輸入緩沖區(qū)提取接收幀進行處理,直到緩沖區(qū)為空時,結(jié)束處理程序的執(zhí)行并重新啟用該設(shè)備的中斷通知。NAPI 結(jié)合了輪詢和中斷的優(yōu)點:
空閑狀態(tài)下,內(nèi)核既不需要浪費資源去做輪詢,也能在設(shè)備接收到新的網(wǎng)絡(luò)幀后立刻得到通知。 內(nèi)核被通知在設(shè)備緩沖區(qū)有待處理的數(shù)據(jù)之后,不需要再浪費資源去處理中斷,簡單通過輪詢?nèi)ヌ幚磉@些數(shù)據(jù)即可。
對內(nèi)核來說,NAPI 有效減少了高負載下需要處理的中斷數(shù)量,因此降低了 CPU 占用,此外通過輪詢地方式去訪問設(shè)備,也能夠減少設(shè)備之間的爭搶。內(nèi)核通過以下數(shù)據(jù)結(jié)構(gòu)來實現(xiàn) NAPI:
poll:用于從設(shè)備的入站隊列中出隊網(wǎng)絡(luò)幀的虛擬函數(shù),每個設(shè)備都會有一個單獨的入站隊列。poll_list: 一個維護處于輪詢中狀態(tài)設(shè)備的鏈表。多個設(shè)備可以共用同一個中斷信號,因此內(nèi)核需要輪詢多個設(shè)備。加入到列表之后來自該設(shè)備的中斷將被禁用。quota和weight:內(nèi)核通過這兩個值來控制每次從設(shè)備中出隊數(shù)據(jù)的數(shù)量,quota 數(shù)量越小意味不同設(shè)備的數(shù)據(jù)幀更有機會得到公平的處理機會,但內(nèi)核會花費更多的時間在設(shè)備之前切換,反之依然。
當設(shè)備發(fā)送中斷信號且被接收之后,內(nèi)核執(zhí)行該設(shè)備驅(qū)動注冊的中斷處理程序。中斷處理程序?qū)⒄{(diào)用 napi_schedule 來調(diào)度輪詢程序的執(zhí)行。在 napi_schedule 中,如果發(fā)送中斷的設(shè)備未在 CPU 的 poll_list 中,內(nèi)核將其加入到 poll_list,并通過 __raise_softirq_irqoff 觸發(fā) NET_RX_SOFTIRQ 軟中斷的調(diào)度。其主要邏輯位于 ____napi_schedule 中:
/*?Called?with?irq?disabled?*/
static?inline?void?____napi_schedule(struct?softnet_data?*sd,
?????????struct?napi_struct?*napi)
{
?list_add_tail(&napi->poll_list,?&sd->poll_list);
?__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
NET_RX_SOFTIRQ 軟中斷處理程序
NET_RX_SOFTIRQ 的處理程序是 net_rx_action。其代碼如下:
static?void?net_rx_action(struct?softirq_action?*h)
{
?struct?softnet_data?*sd?=?&__get_cpu_var(softnet_data);
?unsigned?long?time_limit?=?jiffies?+?2;
?int?budget?=?netdev_budget;
?void?*have;
?local_irq_disable();
?while?(!list_empty(&sd->poll_list))?{
??struct?napi_struct?*n;
??int?work,?weight;
??/*?If?softirq?window?is?exhuasted?then?punt.
???*?Allow?this?to?run?for?2?jiffies?since?which?will?allow
???*?an?average?latency?of?1.5/HZ.
???*/
??if?(unlikely(budget?<=?0?||?time_after(jiffies,?time_limit)))
???goto?softnet_break;
??local_irq_enable();
??/*?Even?though?interrupts?have?been?re-enabled,?this
???*?access?is?safe?because?interrupts?can?only?add?new
???*?entries?to?the?tail?of?this?list,?and?only?->poll()
???*?calls?can?remove?this?head?entry?from?the?list.
???*/
??n?=?list_first_entry(&sd->poll_list,?struct?napi_struct,?poll_list);
??have?=?netpoll_poll_lock(n);
??weight?=?n->weight;
??/*?This?NAPI_STATE_SCHED?test?is?for?avoiding?a?race
???*?with?netpoll's?poll_napi().??Only?the?entity?which
???*?obtains?the?lock?and?sees?NAPI_STATE_SCHED?set?will
???*?actually?make?the?->poll()?call.??Therefore?we?avoid
???*?accidentally?calling?->poll()?when?NAPI?is?not?scheduled.
???*/
??work?=?0;
??if?(test_bit(NAPI_STATE_SCHED,?&n->state))?{
???work?=?n->poll(n,?weight);
???trace_napi_poll(n);
??}
??WARN_ON_ONCE(work?>?weight);
??budget?-=?work;
??local_irq_disable();
??/*?Drivers?must?not?modify?the?NAPI?state?if?they
???*?consume?the?entire?weight.??In?such?cases?this?code
???*?still?"owns"?the?NAPI?instance?and?therefore?can
???*?move?the?instance?around?on?the?list?at-will.
???*/
??if?(unlikely(work?==?weight))?{
???if?(unlikely(napi_disable_pending(n)))?{
????local_irq_enable();
????napi_complete(n);
????local_irq_disable();
???}?else
????list_move_tail(&n->poll_list,?&sd->poll_list);
??}
??netpoll_poll_unlock(have);
?}
out:
?net_rps_action_and_irq_enable(sd);
#ifdef?CONFIG_NET_DMA
?/*
??*?There?may?not?be?any?more?sk_buffs?coming?right?now,?so?push
??*?any?pending?DMA?copies?to?hardware
??*/
?dma_issue_pending_all();
#endif
?return;
softnet_break:
?sd->time_squeeze++;
?__raise_softirq_irqoff(NET_RX_SOFTIRQ);
?goto?out;
}
當 net_rx_action 被調(diào)度執(zhí)行后:
從頭開始遍歷 poll_list鏈表中的設(shè)備,調(diào)用設(shè)備的poll虛擬函數(shù)處理入站隊列中的數(shù)據(jù)幀。poll 被調(diào)用時所處理的數(shù)據(jù)幀數(shù)量到達最大閾值后,即使該設(shè)備的入站隊列還未被清空,也會將該設(shè)備移動到 poll_list的尾部,轉(zhuǎn)而去處理poll_list中的下一個設(shè)備。如果設(shè)備的入站隊列被清空,調(diào)用 netif_rx_complete將設(shè)備移出poll_list并開啟該設(shè)備的中斷通知。一直執(zhí)行該流程直到 poll_list被清空,或者net_rx_action執(zhí)行完了足夠的時間片(為了不過多占用 CPU 資源),這種情況退出前net_rx_action會重新調(diào)度自己的下一次執(zhí)行。
Poll 虛擬函數(shù)
在設(shè)備驅(qū)動的初始化過程中,設(shè)備會將 dev->poll 指向由驅(qū)動提供的自定義函數(shù),因此不同驅(qū)動會使用不同的 poll 函數(shù)。我們將介紹由 Linux 提供的默認 poll 函數(shù) process_backlog,它的工作方式與大多數(shù)驅(qū)動的 poll 函數(shù)相似,其主要的區(qū)別在于,process_backlog 工作時不會禁用中斷,由于非 NAPI 設(shè)備使用一個共享的輸入隊列,因此從輸入隊列中出棧數(shù)據(jù)幀時需要臨時禁用中斷以實現(xiàn)加鎖;而 NAPI 設(shè)備使用單獨的入站隊列,且加入 poll_list 的設(shè)備會被單獨禁用中斷,因此在 poll 時不需要考慮加鎖的問題。
process_backlog 執(zhí)行時,首先計算出該設(shè)備的 quota。然后進入下面的循環(huán)流程:
禁用中斷,從該 CPU 關(guān)聯(lián)的輸入隊列中出棧數(shù)據(jù)幀,然后重新啟用中斷。 如果出棧時發(fā)現(xiàn)輸入隊列已空,則將該設(shè)備移出 poll_list,并結(jié)束執(zhí)行。如果輸入隊列不為空,調(diào)用 netif_receive_skb(skb)處理被出棧的數(shù)據(jù)幀,我們將在下一節(jié)介紹該函數(shù)。檢查以下條件,如果未滿足條件則跳轉(zhuǎn)到步驟 1 繼續(xù)循環(huán): 如果已出棧的數(shù)據(jù)幀數(shù)量達到該設(shè)備的 quota 值,結(jié)束執(zhí)行。 如果已執(zhí)行完了足夠的 CPU 時間片,結(jié)束執(zhí)行。
處理接收幀
netif_receive_skb 是 poll 虛擬函數(shù)用于處理接收幀的工具函數(shù),簡單來說它會依次對數(shù)據(jù)幀做如下處理:
處理數(shù)據(jù)幀的 bond 功能。Linux 能夠?qū)⒁唤M設(shè)備聚合成一個 bond 設(shè)備,數(shù)據(jù)幀在進入三層處理之前,會在此將其接收設(shè)備 skb->dev更改為 bond 中的主設(shè)備。傳遞一份數(shù)據(jù)幀副本給已注冊的各個協(xié)議的嗅探程序。 處理一些需要在二層完成的功能,包括橋接。如果數(shù)據(jù)幀不需要橋接,繼續(xù)向下執(zhí)行。 傳遞一份數(shù)據(jù)幀副本給 skb->protocol對應(yīng)的且已注冊的三層協(xié)議處理程序。至此數(shù)據(jù)幀進入內(nèi)核網(wǎng)絡(luò)棧的更上層。
如果沒有找到對應(yīng)的協(xié)議處理程序或者未被橋接等功能消費,數(shù)據(jù)幀將被內(nèi)核丟棄。
通常來說,三層協(xié)議處理程序會對數(shù)據(jù)幀作如下處理:
將它們傳遞給網(wǎng)絡(luò)協(xié)議棧中更上層的協(xié)議如 TCP, UDP, ICMP,最后傳遞給應(yīng)用進程。 在 netfilter 等數(shù)據(jù)幀處理框架中被丟棄。 如果數(shù)據(jù)幀的目的地不是本地主機,將被轉(zhuǎn)發(fā)到其他機器。
對 Linux 如何接收網(wǎng)絡(luò)幀的討論到此結(jié)束,如果對數(shù)據(jù)幀在三層網(wǎng)絡(luò)棧的處理流程感興趣,可查看作者的另一篇文章 深入理解 netfilter 和 iptables[1]。
參考鏈接
Understanding Linux network internals-Christian Benvenuti -O'Reilly Media Linux 內(nèi)核深度解析 - 余華兵
引用鏈接
深入理解 netfilter 和 iptables: https://www.waynerv.com/posts/understanding-netfilter-and-iptables/
原文鏈接:https://www.waynerv.com/posts/how-linux-process-input-frames/


你可能還喜歡
點擊下方圖片即可閱讀

云原生是一種信仰???
關(guān)注公眾號
后臺回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點擊?"閱讀原文"?獲取更好的閱讀體驗!
發(fā)現(xiàn)朋友圈變“安靜”了嗎?


