天天看點

詳解:Linux 是如何收發網絡包的?

Linux 系統是如何收發網絡包的?

首先要了解網絡模型。

網絡模型

為了使得多種裝置能通過網絡互相通信,和為了解決各種不同裝置在網絡互聯中的相容性問題,國際标準化組織制定了開放式系統互聯通信參考模型(Open System Interconnection Reference Model),也就是 OSI 網絡模型,該模型主要有 7 層,分别是應用層、表示層、會話層、傳輸層、網絡層、資料鍊路層以及實體層。

每一層負責的職能都不同,如下:

  • 應用層,負責給應用程式提供統一的接口;
  • 表示層,負責把資料轉換成相容另一個系統能識别的格式;
  • 會話層,負責建立、管理和終止表示層實體之間的通信會話;
  • 傳輸層,負責端到端的資料傳輸;
  • 網絡層,負責資料的路由、轉發、分片;
  • 資料鍊路層,負責資料的封幀和差錯檢測,以及 MAC 尋址;
  • 實體層,負責在實體網絡中傳輸資料幀;

由于 OSI 模型實在太複雜,提出的也隻是概念理論上的分層,并沒有提供具體的實作方案。

事實上,我們比較常見,也比較實用的是四層模型,即 TCP/IP 網絡模型,Linux 系統正是按照這套網絡模型來實作網絡協定棧的。

TCP/IP 網絡模型共有 4 層,分别是應用層、傳輸層、網絡層和網絡接口層,每一層負責的職能如下:

  • 應用層,負責向使用者提供一組應用程式,比如 HTTP、DNS、FTP 等;
  • 傳輸層,負責端到端的通信,比如 TCP、UDP 等;
  • 網絡層,負責網絡包的封裝、分片、路由、轉發,比如 IP、ICMP 等;
  • 網絡接口層,負責網絡包在實體網絡中的傳輸,比如網絡包的封幀、 MAC 尋址、差錯檢測,以及通過網卡傳輸網絡幀等;

TCP/IP 網絡模型相比 OSI 網絡模型簡化了不少,也更加易記,它們之間的關系如下圖:

詳解:Linux 是如何收發網絡包的?

不過,我們常說的七層和四層負載均衡,是用 OSI 網絡模型來描述的,七層對應的是應用層,四層對應的是傳輸層。

Linux 網絡協定棧

我們可以把自己的身體比作應用層中的資料,打底衣服比作傳輸層中的 TCP 頭,外套比作網絡層中 IP 頭,帽子和鞋子分别比作網絡接口層的幀頭和幀尾。

在冬天這個季節,當我們要從家裡出去玩的時候,自然要先穿個打底衣服,再套上保暖外套,最後穿上帽子和鞋子才出門,這個過程就好像我們把 TCP 協定通信的網絡包發出去的時候,會把應用層的資料按照網絡協定棧層層封裝和處理。

你從下面這張圖可以看到,應用層資料在每一層的封裝格式。

詳解:Linux 是如何收發網絡包的?

其中:

  • 傳輸層,給應用資料前面增加了 TCP 頭;
  • 網絡層,給 TCP 資料包前面增加了 IP 頭;
  • 網絡接口層,給 IP 資料包前後分别增加了幀頭和幀尾;

這些新增的頭部和尾部,都有各自的作用,也都是按照特定的協定格式填充,這每一層都增加了各自的協定頭,那自然網絡包的大小就增大了,但實體鍊路并不能傳輸任意大小的資料包,是以在以太網中,規定了最大傳輸單元(MTU)是 ​

​1500​

​ 位元組,也就是規定了單次傳輸的最大 IP 包大小。

當網絡包超過 MTU 的大小,就會在網絡層分片,以確定分片後的 IP 包不會超過 MTU 大小,如果 MTU 越小,需要的分包就越多,那麼網絡吞吐能力就越差,相反的,如果 MTU 越大,需要的分包就越少,那麼網絡吞吐能力就越好。

知道了 TCP/IP 網絡模型,以及網絡包的封裝原理後,那麼 Linux 網絡協定棧的樣子,你想必猜到了大概,它其實就類似于 TCP/IP 的四層結構:

詳解:Linux 是如何收發網絡包的?

從上圖的的網絡協定棧,你可以看到:

  • 應用程式需要通過系統調用,來跟 Socket 層進行資料互動;
  • Socket 層的下面就是傳輸層、網絡層和網絡接口層;
  • 最下面的一層,則是網卡驅動程式和硬體網卡裝置;

Linux 接收網絡包的流程

網卡是計算機裡的一個硬體,專門負責接收和發送網絡包,當網卡接收到一個網絡包後,會通過 DMA 技術,将網絡包寫入到指定的記憶體位址,也就是寫入到 Ring Buffer ,這個是一個環形緩沖區,接着就會告訴作業系統這個網絡包已經到達。

那應該怎麼告訴作業系統這個網絡包已經到達了呢?

最簡單的一種方式就是觸發中斷,也就是每當網卡收到一個網絡包,就觸發一個中斷告訴作業系統。

但是,這存在一個問題,在高性能網絡場景下,網絡包的數量會非常多,那麼就會觸發非常多的中斷,要知道當 CPU 收到了中斷,就會停下手裡的事情,而去處理這些網絡包,處理完畢後,才會回去繼續其他事情,那麼頻繁地觸發中斷,則會導緻 CPU 一直沒完沒了的進行中斷,而導緻其他任務可能無法繼續前進,進而影響系統的整體效率。

是以為了解決頻繁中斷帶來的性能開銷,Linux 核心在 2.6 版本中引入了 NAPI 機制,它是混合「中斷和輪詢」的方式來接收網絡包,它的核心概念就是不采用中斷的方式讀取資料,而是首先采用中斷喚醒資料接收的服務程式,然後 ​

​poll​

​ 的方法來輪詢資料。

是以,當有網絡包到達時,會通過 DMA 技術,将網絡包寫入到指定的記憶體位址,接着網卡向 CPU 發起硬體中斷,當 CPU 收到硬體中斷請求後,根據中斷表,調用已經注冊的中斷處理函數。

硬體中斷處理函數會做如下的事情:

  • 需要先「暫時屏蔽中斷」,表示已經知道記憶體中有資料了,告訴網卡下次再收到資料包直接寫記憶體就可以了,不要再通知 CPU 了,這樣可以提高效率,避免 CPU 不停的被中斷。
  • 接着,發起「軟中斷」,然後恢複剛才屏蔽的中斷。

至此,硬體中斷處理函數的工作就已經完成。

硬體中斷處理函數做的事情很少,主要耗時的工作都交給軟中斷處理函數了。

軟中斷的處理

核心中的 ksoftirqd 線程專門負責軟中斷的處理,當 ksoftirqd 核心線程收到軟中斷後,就會來輪詢處理資料。

ksoftirqd 線程會從 Ring Buffer 中擷取一個資料幀,用 sk_buff 表示,進而可以作為一個網絡包交給網絡協定棧進行逐層處理。

網絡協定棧

首先,會先進入到網絡接口層,在這一層會檢查封包的合法性,如果不合法則丢棄,合法則會找出該網絡包的上層協定的類型,比如是 IPv4,還是 IPv6,接着再去掉幀頭和幀尾,然後交給網絡層。

到了網絡層,則取出 IP 包,判斷網絡包下一步的走向,比如是交給上層處理還是轉發出去。當确認這個網絡包要發送給本機後,就會從 IP 頭裡看看上一層協定的類型是 TCP 還是 UDP,接着去掉 IP 頭,然後交給傳輸層。

傳輸層取出 TCP 頭或 UDP 頭,根據四元組「源 IP、源端口、目的 IP、目的端口」 作為辨別,找出對應的 Socket,并把資料放到 Socket 的接收緩沖區。

最後,應用層程式調用 Socket 接口,将核心的 Socket 接收緩沖區的資料「拷貝」到應用層的緩沖區,然後喚醒使用者程序。

至此,一個網絡包的接收過程就已經結束了,你也可以從下圖左邊部分看到網絡包接收的流程,右邊部分剛好反過來,它是網絡包發送的流程。

詳解:Linux 是如何收發網絡包的?

Linux 發送網絡包的流程

如上圖的右半部分,發送網絡包的流程正好和接收流程相反。

首先,應用程式會調用 Socket 發送資料包的接口,由于這個是系統調用,是以會從使用者态陷入到核心态中的 Socket 層,核心會申請一個核心态的 sk_buff 記憶體,将使用者待發送的資料拷貝到 sk_buff 記憶體,并将其加入到發送緩沖區。

接下來,網絡協定棧從 Socket 發送緩沖區中取出 sk_buff,并按照 TCP/IP 協定棧從上到下逐層處理。

如果使用的是 TCP 傳輸協定發送資料,那麼先拷貝一個新的 sk_buff 副本 ,這是因為 sk_buff 後續在調用網絡層,最後到達網卡發送完成的時候,這個 sk_buff 會被釋放掉。而 TCP 協定是支援丢失重傳的,在收到對方的 ACK 之前,這個 sk_buff 不能被删除。是以核心的做法就是每次調用網卡發送的時候,實際上傳遞出去的是 sk_buff 的一個拷貝,等收到 ACK 再真正删除。

接着,對 sk_buff 填充 TCP 頭。這裡提一下,sk_buff 可以表示各個層的資料包,在應用層資料包叫 data,在 TCP 層我們稱為 segment,在 IP 層我們叫 packet,在資料鍊路層稱為 frame。

你可能會好奇,為什麼全部資料包隻用一個結構體來描述呢?協定棧采用的是分層結構,上層向下層傳遞資料時需要增加標頭,下層向上層資料時又需要去掉標頭,如果每一層都用一個結構體,那在層之間傳遞資料的時候,就要發生多次拷貝,這将大大降低 CPU 效率。

于是,為了在層級之間傳遞資料時,不發生拷貝,隻用 sk_buff 一個結構體來描述所有的網絡包,那它是如何做到的呢?是通過調整 sk_buff 中 ​

​data​

​ 的指針,比如:

  • 當接收封包時,從網卡驅動開始,通過協定棧層層往上傳送資料報,通過增加 skb->data 的值,來逐漸剝離協定首部。
  • 當要發送封包時,建立 sk_buff 結構體,資料緩存區的頭部預留足夠的空間,用來填充各層首部,在經過各下層協定時,通過減少 skb->data 的值來增加協定首部。

你可以從下面這張圖看到,當發送封包時,data 指針的移動過程。

詳解:Linux 是如何收發網絡包的?

至此,傳輸層的工作也就都完成了。

然後交給網絡層,在網絡層裡會做這些工作:選取路由(确認下一跳的 IP)、填充 IP 頭、netfilter 過濾、對超過 MTU 大小的資料包進行分片。處理完這些工作後會交給網絡接口層處理。

網絡接口層會通過 ARP 協定獲得下一跳的 MAC 位址,然後對 sk_buff 填充幀頭和幀尾,接着将 sk_buff 放到網卡的發送隊列中。

這一些工作準備好後,會觸發「軟中斷」告訴網卡驅動程式,這裡有新的網絡包需要發送,驅動程式會從發送隊列中讀取 sk_buff,将這個 sk_buff 挂到 RingBuffer 中,接着将 sk_buff 資料映射到網卡可通路的記憶體 DMA 區域,最後觸發真實的發送。

當資料發送完成以後,其實工作并沒有結束,因為記憶體還沒有清理。當發送完成的時候,網卡裝置會觸發一個硬中斷來釋放記憶體,主要是釋放 sk_buff 記憶體和清理 RingBuffer 記憶體。

最後,當收到這個 TCP 封包的 ACK 應答時,傳輸層就會釋放原始的 sk_buff 。

發送網絡資料的時候,涉及幾次記憶體拷貝操作?

第一次,調用發送資料的系統調用的時候,核心會申請一個核心态的 sk_buff 記憶體,将使用者待發送的資料拷貝到 sk_buff 記憶體,并将其加入到發送緩沖區。

第二次,在使用 TCP 傳輸協定的情況下,從傳輸層進入網絡層的時候,每一個 sk_buff 都會被克隆一個新的副本出來。副本 sk_buff 會被送往網絡層,等它發送完的時候就會釋放掉,然後原始的 sk_buff 還保留在傳輸層,目的是為了實作 TCP 的可靠傳輸,等收到這個資料包的 ACK 時,才會釋放原始的 sk_buff 。

第三次,當 IP 層發現 sk_buff 大于 MTU 時才需要進行。會再申請額外的 sk_buff,并将原來的 sk_buff 拷貝為多個小的 sk_buff。

總結

電腦與電腦之間通常都是通過網卡、交換機、路由器等網絡裝置連接配接到一起,那由于網絡裝置的異構性,國際标準化組織定義了一個七層的 OSI 網絡模型,但是這個模型由于比較複雜,實際應用中并沒有采用,而是采用了更為簡化的 TCP/IP 模型,Linux 網絡協定棧就是按照了該模型來實作的。

繼續閱讀