<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 13225字,需瀏覽 27分鐘

           ·

          2022-04-19 01:10



          本文將從初學(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)為例:

          1. 設(shè)備會將多個事件歸類為一種中斷類型(甚至還可以在發(fā)送中斷信號前等待一段時間,將多個中斷聚合成一個信號發(fā)送)。中斷觸發(fā) vortex_interrupt 的執(zhí)行并禁用該 CPU 上的中斷。
          2. 如果中斷是由接收幀事件 RxComplete 引發(fā),處理程序調(diào)用其他代碼處理設(shè)備接收的幀。
          3. vortex_interrupt 在執(zhí)行期間持續(xù)讀取設(shè)備寄存器,檢查是否有新的中斷信號發(fā)出。如果有且中斷事件為 RxComplete,處理程序?qū)⒗^續(xù)處理接收幀,直到已處理幀的數(shù)量達到預(yù)設(shè)的 work_done值才結(jié)束。而其他類型的中斷將被處理程序忽略。

          軟中斷處理機制

          一個中斷通常會觸發(fā)以下事件:

          1. 設(shè)備產(chǎn)生一個中斷并通過硬件通知內(nèi)核。
          2. 如果內(nèi)核沒有正在處理另一個中斷(即中斷沒有被禁用),它將收到這個通知。
          3. 內(nèi)核禁用本地 CPU 的中斷,并執(zhí)行與收到的中斷類型相關(guān)聯(lián)的處理程序。
          4. 內(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 處于中斷上下文(即禁用中斷)的時間:

          1. 設(shè)備向 CPU 發(fā)出中斷信號,通知它有特定事件發(fā)生。
          2. CPU 執(zhí)行中斷相關(guān)的上半部處理函數(shù),禁用之后的中斷通知,直到處理程序完成工作:a. 將一些數(shù)據(jù)保存在內(nèi)存中,用于內(nèi)核在之后進一步處理中斷事件。b. 設(shè)置一個標志位,以確保內(nèi)核知道有待處理的中斷。c. 在終止之前重新啟用本地 CPU 的中斷通知。
          3. 在之后的某個時間點,當內(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_SOFTIRQNET_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)度:

          1. __raise_softirq_irqoff:在一個專門的 bitmap (位圖)結(jié)構(gòu)中設(shè)置與軟中斷類型對應(yīng)的比特位,當后續(xù)對該比特位的檢查結(jié)果為真時,調(diào)用與軟中斷關(guān)聯(lián)的處理程序。每個 CPU 使用一個單獨的 bitmap。
          2. raise_softirq_irqoff:內(nèi)部包裝了 __raise_softirq_irqoff 函數(shù)。如果此函數(shù)不是從中斷上下文中調(diào)用,且搶占未被禁用,將會額外調(diào)度一個 ksoftirqd 線程。
          3. raise_softirq: 內(nèi)部包裝了 raise_softirq_irqoff,但執(zhí)行時會禁用 CPU 中斷。

          在特定的時機,內(nèi)核會檢查每個 CPU 獨有的 bitmap 判斷是否有已調(diào)度的軟中斷等待執(zhí)行,如果有將會調(diào)用 do_softirq 處理軟中斷。內(nèi)核處理軟中斷的時機如下:

          1. 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();
          }
          1. 從中斷和異常事件(包括系統(tǒng)調(diào)用)返回時,這部分處理邏輯直接寫入了匯編代碼。
          2. 調(diào)用 local_bh_enable 開啟軟中斷時,將執(zhí)行待處理的軟中斷。
          3. 每個處理器有一個軟中斷線程 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 的核心邏輯),只有達到以下兩種條件時才會停止:

          1. 沒有待處理的軟中斷時,此時 ksoftirqd 會調(diào)用 schedule() 觸發(fā)調(diào)度主動讓出 CPU 資源。
          2. 該線程執(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í)行。具體來說中斷處理程序會做這些事情:

          1. 將網(wǎng)絡(luò)幀復(fù)制到 sk_buff 數(shù)據(jù)結(jié)構(gòu)中。
          2. 初始化一些 sk_buff 的參數(shù),供上層的網(wǎng)絡(luò)棧使用。特別是 skb->protocol,它標識了上層的協(xié)議處理程序。
          3. 更新其他的設(shè)備專用參數(shù)。
          4. 通過調(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)點:

          1. 空閑狀態(tài)下,內(nèi)核既不需要浪費資源去做輪詢,也能在設(shè)備接收到新的網(wǎng)絡(luò)幀后立刻得到通知。
          2. 內(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:

          1. poll:用于從設(shè)備的入站隊列中出隊網(wǎng)絡(luò)幀的虛擬函數(shù),每個設(shè)備都會有一個單獨的入站隊列。
          2. poll_list: 一個維護處于輪詢中狀態(tài)設(shè)備的鏈表。多個設(shè)備可以共用同一個中斷信號,因此內(nèi)核需要輪詢多個設(shè)備。加入到列表之后來自該設(shè)備的中斷將被禁用。
          3. quotaweight:內(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í)行后:

          1. 從頭開始遍歷 poll_list 鏈表中的設(shè)備,調(diào)用設(shè)備的 poll 虛擬函數(shù)處理入站隊列中的數(shù)據(jù)幀。
          2. poll 被調(diào)用時所處理的數(shù)據(jù)幀數(shù)量到達最大閾值后,即使該設(shè)備的入站隊列還未被清空,也會將該設(shè)備移動到 poll_list 的尾部,轉(zhuǎn)而去處理 poll_list 中的下一個設(shè)備。
          3. 如果設(shè)備的入站隊列被清空,調(diào)用 netif_rx_complete 將設(shè)備移出 poll_list 并開啟該設(shè)備的中斷通知。
          4. 一直執(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)流程:

          1. 禁用中斷,從該 CPU 關(guān)聯(lián)的輸入隊列中出棧數(shù)據(jù)幀,然后重新啟用中斷。
          2. 如果出棧時發(fā)現(xiàn)輸入隊列已空,則將該設(shè)備移出 poll_list,并結(jié)束執(zhí)行。
          3. 如果輸入隊列不為空,調(diào)用 netif_receive_skb(skb) 處理被出棧的數(shù)據(jù)幀,我們將在下一節(jié)介紹該函數(shù)。
          4. 檢查以下條件,如果未滿足條件則跳轉(zhuǎn)到步驟 1 繼續(xù)循環(huán):
            1. 如果已出棧的數(shù)據(jù)幀數(shù)量達到該設(shè)備的 quota 值,結(jié)束執(zhí)行。
            2. 如果已執(zhí)行完了足夠的 CPU 時間片,結(jié)束執(zhí)行。

          處理接收幀

          netif_receive_skb 是 poll 虛擬函數(shù)用于處理接收幀的工具函數(shù),簡單來說它會依次對數(shù)據(jù)幀做如下處理:

          1. 處理數(shù)據(jù)幀的 bond 功能。Linux 能夠?qū)⒁唤M設(shè)備聚合成一個 bond 設(shè)備,數(shù)據(jù)幀在進入三層處理之前,會在此將其接收設(shè)備 skb->dev 更改為 bond 中的主設(shè)備。
          2. 傳遞一份數(shù)據(jù)幀副本給已注冊的各個協(xié)議的嗅探程序。
          3. 處理一些需要在二層完成的功能,包括橋接。如果數(shù)據(jù)幀不需要橋接,繼續(xù)向下執(zhí)行。
          4. 傳遞一份數(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)核深度解析 - 余華兵

          引用鏈接

          [1]

          深入理解 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)朋友圈變“安靜”了嗎?

          瀏覽 34
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲视频在线观看自 | 操逼骚逼网站 | 哪里可以免费看A片 | www,操逼 | 古典武侠区伊人一区人妻在线 |