作者:小林coding
大家好,我是小林。
之前寫(xiě)過(guò)一篇:你不好奇 Linux 是如何收發(fā)網(wǎng)絡(luò)包的? 文章。
當(dāng)時(shí)有些地方寫(xiě)的比較籠統(tǒng),然后我「把 Linux 接收+發(fā)送網(wǎng)絡(luò)包的流程」這部分內(nèi)容完善了下,現(xiàn)在重新分享給大家。
發(fā)車(chē)發(fā)車(chē)!
網(wǎng)絡(luò)模型
為了使得多種設(shè)備能通過(guò)網(wǎng)絡(luò)相互通信,和為了解決各種不同設(shè)備在網(wǎng)絡(luò)互聯(lián)中的兼容性問(wèn)題。
國(guó)際標(biāo)準(zhǔn)化組織制定了開(kāi)放式系統(tǒng)互聯(lián)通信參考模型(Open System Interconnection Reference Model),也就是 OSI 網(wǎng)絡(luò)模型。
該模型主要有 7 層,分別是應(yīng)用層、表示層、會(huì)話層、傳輸層、網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層以及物理層。
每一層負(fù)責(zé)的職能都不同,如下:
- 應(yīng)用層,負(fù)責(zé)給應(yīng)用程序提供統(tǒng)一的接口;表示層,負(fù)責(zé)把數(shù)據(jù)轉(zhuǎn)換成兼容另一個(gè)系統(tǒng)能識(shí)別的格式;會(huì)話層,負(fù)責(zé)建立、管理和終止表示層實(shí)體之間的通信會(huì)話;傳輸層,負(fù)責(zé)端到端的數(shù)據(jù)傳輸;網(wǎng)絡(luò)層,負(fù)責(zé)數(shù)據(jù)的路由、轉(zhuǎn)發(fā)、分片;數(shù)據(jù)鏈路層,負(fù)責(zé)數(shù)據(jù)的封幀和差錯(cuò)檢測(cè),以及 MAC 尋址;物理層,負(fù)責(zé)在物理網(wǎng)絡(luò)中傳輸數(shù)據(jù)幀;
由于 OSI 模型實(shí)在太復(fù)雜,提出的也只是概念理論上的分層,并沒(méi)有提供具體的實(shí)現(xiàn)方案。
事實(shí)上,我們比較常見(jiàn),也比較實(shí)用的是四層模型,即 TCP/IP 網(wǎng)絡(luò)模型,Linux 系統(tǒng)正是按照這套網(wǎng)絡(luò)模型來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)協(xié)議棧的。
TCP/IP 網(wǎng)絡(luò)模型共有 4 層,分別是應(yīng)用層、傳輸層、網(wǎng)絡(luò)層和網(wǎng)絡(luò)接口層,每一層負(fù)責(zé)的職能如下:
- 應(yīng)用層,負(fù)責(zé)向用戶提供一組應(yīng)用程序,比如 HTTP、DNS、FTP 等;傳輸層,負(fù)責(zé)端到端的通信,比如 TCP、UDP 等;網(wǎng)絡(luò)層,負(fù)責(zé)網(wǎng)絡(luò)包的封裝、分片、路由、轉(zhuǎn)發(fā),比如 IP、ICMP 等;網(wǎng)絡(luò)接口層,負(fù)責(zé)網(wǎng)絡(luò)包在物理網(wǎng)絡(luò)中的傳輸,比如網(wǎng)絡(luò)包的封幀、 MAC 尋址、差錯(cuò)檢測(cè),以及通過(guò)網(wǎng)卡傳輸網(wǎng)絡(luò)幀等;
TCP/IP 網(wǎng)絡(luò)模型相比 OSI 網(wǎng)絡(luò)模型簡(jiǎn)化了不少,也更加易記,它們之間的關(guān)系如下圖:
不過(guò),我們常說(shuō)的七層和四層負(fù)載均衡,是用 OSI 網(wǎng)絡(luò)模型來(lái)描述的,七層對(duì)應(yīng)的是應(yīng)用層,四層對(duì)應(yīng)的是傳輸層。
Linux 網(wǎng)絡(luò)協(xié)議棧
我們可以把自己的身體比作應(yīng)用層中的數(shù)據(jù),打底衣服比作傳輸層中的 TCP 頭,外套比作網(wǎng)絡(luò)層中 IP 頭,帽子和鞋子分別比作網(wǎng)絡(luò)接口層的幀頭和幀尾。
在冬天這個(gè)季節(jié),當(dāng)我們要從家里出去玩的時(shí)候,自然要先穿個(gè)打底衣服,再套上保暖外套,最后穿上帽子和鞋子才出門(mén),這個(gè)過(guò)程就好像我們把 TCP 協(xié)議通信的網(wǎng)絡(luò)包發(fā)出去的時(shí)候,會(huì)把應(yīng)用層的數(shù)據(jù)按照網(wǎng)絡(luò)協(xié)議棧層層封裝和處理。
你從下面這張圖可以看到,應(yīng)用層數(shù)據(jù)在每一層的封裝格式。
其中:
- 傳輸層,給應(yīng)用數(shù)據(jù)前面增加了 TCP 頭;網(wǎng)絡(luò)層,給 TCP 數(shù)據(jù)包前面增加了 IP 頭;網(wǎng)絡(luò)接口層,給 IP 數(shù)據(jù)包前后分別增加了幀頭和幀尾;
這些新增的頭部和尾部,都有各自的作用,也都是按照特定的協(xié)議格式填充,這每一層都增加了各自的協(xié)議頭,那自然網(wǎng)絡(luò)包的大小就增大了,但物理鏈路并不能傳輸任意大小的數(shù)據(jù)包,所以在以太網(wǎng)中,規(guī)定了最大傳輸單元(MTU)是 1500
字節(jié),也就是規(guī)定了單次傳輸?shù)淖畲?IP 包大小。
當(dāng)網(wǎng)絡(luò)包超過(guò) MTU 的大小,就會(huì)在網(wǎng)絡(luò)層分片,以確保分片后的 IP 包不會(huì)超過(guò) MTU 大小,如果 MTU 越小,需要的分包就越多,那么網(wǎng)絡(luò)吞吐能力就越差,相反的,如果 MTU 越大,需要的分包就越少,那么網(wǎng)絡(luò)吞吐能力就越好。
知道了 TCP/IP 網(wǎng)絡(luò)模型,以及網(wǎng)絡(luò)包的封裝原理后,那么 Linux 網(wǎng)絡(luò)協(xié)議棧的樣子,你想必猜到了大概,它其實(shí)就類(lèi)似于 TCP/IP 的四層結(jié)構(gòu):
從上圖的的網(wǎng)絡(luò)協(xié)議棧,你可以看到:
- 應(yīng)用程序需要通過(guò)系統(tǒng)調(diào)用,來(lái)跟 Socket 層進(jìn)行數(shù)據(jù)交互;Socket 層的下面就是傳輸層、網(wǎng)絡(luò)層和網(wǎng)絡(luò)接口層;最下面的一層,則是網(wǎng)卡驅(qū)動(dòng)程序和硬件網(wǎng)卡設(shè)備;
Linux 接收網(wǎng)絡(luò)包的流程
網(wǎng)卡是計(jì)算機(jī)里的一個(gè)硬件,專(zhuān)門(mén)負(fù)責(zé)接收和發(fā)送網(wǎng)絡(luò)包,當(dāng)網(wǎng)卡接收到一個(gè)網(wǎng)絡(luò)包后,會(huì)通過(guò) DMA 技術(shù),將網(wǎng)絡(luò)包寫(xiě)入到指定的內(nèi)存地址,也就是寫(xiě)入到 Ring Buffer ,這個(gè)是一個(gè)環(huán)形緩沖區(qū),接著就會(huì)告訴操作系統(tǒng)這個(gè)網(wǎng)絡(luò)包已經(jīng)到達(dá)。
那應(yīng)該怎么告訴操作系統(tǒng)這個(gè)網(wǎng)絡(luò)包已經(jīng)到達(dá)了呢?
最簡(jiǎn)單的一種方式就是觸發(fā)中斷,也就是每當(dāng)網(wǎng)卡收到一個(gè)網(wǎng)絡(luò)包,就觸發(fā)一個(gè)中斷告訴操作系統(tǒng)。
但是,這存在一個(gè)問(wèn)題,在高性能網(wǎng)絡(luò)場(chǎng)景下,網(wǎng)絡(luò)包的數(shù)量會(huì)非常多,那么就會(huì)觸發(fā)非常多的中斷,要知道當(dāng) CPU 收到了中斷,就會(huì)停下手里的事情,而去處理這些網(wǎng)絡(luò)包,處理完畢后,才會(huì)回去繼續(xù)其他事情,那么頻繁地觸發(fā)中斷,則會(huì)導(dǎo)致 CPU 一直沒(méi)完沒(méi)了的處理中斷,而導(dǎo)致其他任務(wù)可能無(wú)法繼續(xù)前進(jìn),從而影響系統(tǒng)的整體效率。
所以為了解決頻繁中斷帶來(lái)的性能開(kāi)銷(xiāo),Linux 內(nèi)核在 2.6 版本中引入了 NAPI 機(jī)制,它是混合「中斷和輪詢」的方式來(lái)接收網(wǎng)絡(luò)包,它的核心概念就是不采用中斷的方式讀取數(shù)據(jù),而是首先采用中斷喚醒數(shù)據(jù)接收的服務(wù)程序,然后 poll
的方法來(lái)輪詢數(shù)據(jù)。
因此,當(dāng)有網(wǎng)絡(luò)包到達(dá)時(shí),會(huì)通過(guò) DMA 技術(shù),將網(wǎng)絡(luò)包寫(xiě)入到指定的內(nèi)存地址,接著網(wǎng)卡向 CPU 發(fā)起硬件中斷,當(dāng) CPU 收到硬件中斷請(qǐng)求后,根據(jù)中斷表,調(diào)用已經(jīng)注冊(cè)的中斷處理函數(shù)。
硬件中斷處理函數(shù)會(huì)做如下的事情:
- 需要先「暫時(shí)屏蔽中斷」,表示已經(jīng)知道內(nèi)存中有數(shù)據(jù)了,告訴網(wǎng)卡下次再收到數(shù)據(jù)包直接寫(xiě)內(nèi)存就可以了,不要再通知 CPU 了,這樣可以提高效率,避免 CPU 不停的被中斷。接著,發(fā)起「軟中斷」,然后恢復(fù)剛才屏蔽的中斷。
至此,硬件中斷處理函數(shù)的工作就已經(jīng)完成。
硬件中斷處理函數(shù)做的事情很少,主要耗時(shí)的工作都交給軟中斷處理函數(shù)了。
軟中斷的處理
內(nèi)核中的 ksoftirqd 線程專(zhuān)門(mén)負(fù)責(zé)軟中斷的處理,當(dāng) ksoftirqd 內(nèi)核線程收到軟中斷后,就會(huì)來(lái)輪詢處理數(shù)據(jù)。
ksoftirqd 線程會(huì)從 Ring Buffer 中獲取一個(gè)數(shù)據(jù)幀,用 sk_buff 表示,從而可以作為一個(gè)網(wǎng)絡(luò)包交給網(wǎng)絡(luò)協(xié)議棧進(jìn)行逐層處理。
網(wǎng)絡(luò)協(xié)議棧
首先,會(huì)先進(jìn)入到網(wǎng)絡(luò)接口層,在這一層會(huì)檢查報(bào)文的合法性,如果不合法則丟棄,合法則會(huì)找出該網(wǎng)絡(luò)包的上層協(xié)議的類(lèi)型,比如是 IPv4,還是 IPv6,接著再去掉幀頭和幀尾,然后交給網(wǎng)絡(luò)層。
到了網(wǎng)絡(luò)層,則取出 IP 包,判斷網(wǎng)絡(luò)包下一步的走向,比如是交給上層處理還是轉(zhuǎn)發(fā)出去。當(dāng)確認(rèn)這個(gè)網(wǎng)絡(luò)包要發(fā)送給本機(jī)后,就會(huì)從 IP 頭里看看上一層協(xié)議的類(lèi)型是 TCP 還是 UDP,接著去掉 IP 頭,然后交給傳輸層。
傳輸層取出 TCP 頭或 UDP 頭,根據(jù)四元組「源 IP、源端口、目的 IP、目的端口」 作為標(biāo)識(shí),找出對(duì)應(yīng)的 Socket,并把數(shù)據(jù)放到 Socket 的接收緩沖區(qū)。
最后,應(yīng)用層程序調(diào)用 Socket 接口,將內(nèi)核的 Socket 接收緩沖區(qū)的數(shù)據(jù)「拷貝」到應(yīng)用層的緩沖區(qū),然后喚醒用戶進(jìn)程。
至此,一個(gè)網(wǎng)絡(luò)包的接收過(guò)程就已經(jīng)結(jié)束了,你也可以從下圖左邊部分看到網(wǎng)絡(luò)包接收的流程,右邊部分剛好反過(guò)來(lái),它是網(wǎng)絡(luò)包發(fā)送的流程。
Linux 發(fā)送網(wǎng)絡(luò)包的流程
如上圖的右半部分,發(fā)送網(wǎng)絡(luò)包的流程正好和接收流程相反。
首先,應(yīng)用程序會(huì)調(diào)用 Socket 發(fā)送數(shù)據(jù)包的接口,由于這個(gè)是系統(tǒng)調(diào)用,所以會(huì)從用戶態(tài)陷入到內(nèi)核態(tài)中的 Socket 層,內(nèi)核會(huì)申請(qǐng)一個(gè)內(nèi)核態(tài)的 sk_buff 內(nèi)存,將用戶待發(fā)送的數(shù)據(jù)拷貝到 sk_buff 內(nèi)存,并將其加入到發(fā)送緩沖區(qū)。
接下來(lái),網(wǎng)絡(luò)協(xié)議棧從 Socket 發(fā)送緩沖區(qū)中取出 sk_buff,并按照 TCP/IP 協(xié)議棧從上到下逐層處理。
如果使用的是 TCP 傳輸協(xié)議發(fā)送數(shù)據(jù),那么先拷貝一個(gè)新的 sk_buff 副本 ,這是因?yàn)?sk_buff 后續(xù)在調(diào)用網(wǎng)絡(luò)層,最后到達(dá)網(wǎng)卡發(fā)送完成的時(shí)候,這個(gè) sk_buff 會(huì)被釋放掉。而 TCP 協(xié)議是支持丟失重傳的,在收到對(duì)方的 ACK 之前,這個(gè) sk_buff 不能被刪除。所以內(nèi)核的做法就是每次調(diào)用網(wǎng)卡發(fā)送的時(shí)候,實(shí)際上傳遞出去的是 sk_buff 的一個(gè)拷貝,等收到 ACK 再真正刪除。
接著,對(duì) sk_buff 填充 TCP 頭。這里提一下,sk_buff 可以表示各個(gè)層的數(shù)據(jù)包,在應(yīng)用層數(shù)據(jù)包叫 data,在 TCP 層我們稱(chēng)為 segment,在 IP 層我們叫 packet,在數(shù)據(jù)鏈路層稱(chēng)為 frame。
你可能會(huì)好奇,為什么全部數(shù)據(jù)包只用一個(gè)結(jié)構(gòu)體來(lái)描述呢?協(xié)議棧采用的是分層結(jié)構(gòu),上層向下層傳遞數(shù)據(jù)時(shí)需要增加包頭,下層向上層數(shù)據(jù)時(shí)又需要去掉包頭,如果每一層都用一個(gè)結(jié)構(gòu)體,那在層之間傳遞數(shù)據(jù)的時(shí)候,就要發(fā)生多次拷貝,這將大大降低 CPU 效率。
于是,為了在層級(jí)之間傳遞數(shù)據(jù)時(shí),不發(fā)生拷貝,只用 sk_buff 一個(gè)結(jié)構(gòu)體來(lái)描述所有的網(wǎng)絡(luò)包,那它是如何做到的呢?是通過(guò)調(diào)整 sk_buff 中 data
的指針,比如:
- 當(dāng)接收?qǐng)?bào)文時(shí),從網(wǎng)卡驅(qū)動(dòng)開(kāi)始,通過(guò)協(xié)議棧層層往上傳送數(shù)據(jù)報(bào),通過(guò)增加 skb->data 的值,來(lái)逐步剝離協(xié)議首部。當(dāng)要發(fā)送報(bào)文時(shí),創(chuàng)建 sk_buff 結(jié)構(gòu)體,數(shù)據(jù)緩存區(qū)的頭部預(yù)留足夠的空間,用來(lái)填充各層首部,在經(jīng)過(guò)各下層協(xié)議時(shí),通過(guò)減少 skb->data 的值來(lái)增加協(xié)議首部。
你可以從下面這張圖看到,當(dāng)發(fā)送報(bào)文時(shí),data 指針的移動(dòng)過(guò)程。
至此,傳輸層的工作也就都完成了。
然后交給網(wǎng)絡(luò)層,在網(wǎng)絡(luò)層里會(huì)做這些工作:選取路由(確認(rèn)下一跳的 IP)、填充 IP 頭、netfilter 過(guò)濾、對(duì)超過(guò) MTU 大小的數(shù)據(jù)包進(jìn)行分片。處理完這些工作后會(huì)交給網(wǎng)絡(luò)接口層處理。
網(wǎng)絡(luò)接口層會(huì)通過(guò) ARP 協(xié)議獲得下一跳的 MAC 地址,然后對(duì) sk_buff 填充幀頭和幀尾,接著將 sk_buff 放到網(wǎng)卡的發(fā)送隊(duì)列中。
這一些工作準(zhǔn)備好后,會(huì)觸發(fā)「軟中斷」告訴網(wǎng)卡驅(qū)動(dòng)程序,這里有新的網(wǎng)絡(luò)包需要發(fā)送,驅(qū)動(dòng)程序會(huì)從發(fā)送隊(duì)列中讀取 sk_buff,將這個(gè) sk_buff 掛到 RingBuffer 中,接著將 sk_buff 數(shù)據(jù)映射到網(wǎng)卡可訪問(wèn)的內(nèi)存 DMA 區(qū)域,最后觸發(fā)真實(shí)的發(fā)送。
當(dāng)數(shù)據(jù)發(fā)送完成以后,其實(shí)工作并沒(méi)有結(jié)束,因?yàn)閮?nèi)存還沒(méi)有清理。當(dāng)發(fā)送完成的時(shí)候,網(wǎng)卡設(shè)備會(huì)觸發(fā)一個(gè)硬中斷來(lái)釋放內(nèi)存,主要是釋放 sk_buff 內(nèi)存和清理 RingBuffer 內(nèi)存。
最后,當(dāng)收到這個(gè) TCP 報(bào)文的 ACK 應(yīng)答時(shí),傳輸層就會(huì)釋放原始的 sk_buff 。
發(fā)送網(wǎng)絡(luò)數(shù)據(jù)的時(shí)候,涉及幾次內(nèi)存拷貝操作?
第一次,調(diào)用發(fā)送數(shù)據(jù)的系統(tǒng)調(diào)用的時(shí)候,內(nèi)核會(huì)申請(qǐng)一個(gè)內(nèi)核態(tài)的 sk_buff 內(nèi)存,將用戶待發(fā)送的數(shù)據(jù)拷貝到 sk_buff 內(nèi)存,并將其加入到發(fā)送緩沖區(qū)。
第二次,在使用 TCP 傳輸協(xié)議的情況下,從傳輸層進(jìn)入網(wǎng)絡(luò)層的時(shí)候,每一個(gè) sk_buff 都會(huì)被克隆一個(gè)新的副本出來(lái)。副本 sk_buff 會(huì)被送往網(wǎng)絡(luò)層,等它發(fā)送完的時(shí)候就會(huì)釋放掉,然后原始的 sk_buff 還保留在傳輸層,目的是為了實(shí)現(xiàn) TCP 的可靠傳輸,等收到這個(gè)數(shù)據(jù)包的 ACK 時(shí),才會(huì)釋放原始的 sk_buff 。
第三次,當(dāng) IP 層發(fā)現(xiàn) sk_buff 大于 MTU 時(shí)才需要進(jìn)行。會(huì)再申請(qǐng)額外的 sk_buff,并將原來(lái)的 sk_buff 拷貝為多個(gè)小的 sk_buff。
總結(jié)
電腦與電腦之間通常都是通過(guò)話網(wǎng)卡、交換機(jī)、路由器等網(wǎng)絡(luò)設(shè)備連接到一起,那由于網(wǎng)絡(luò)設(shè)備的異構(gòu)性,國(guó)際標(biāo)準(zhǔn)化組織定義了一個(gè)七層的 OSI 網(wǎng)絡(luò)模型,但是這個(gè)模型由于比較復(fù)雜,實(shí)際應(yīng)用中并沒(méi)有采用,而是采用了更為簡(jiǎn)化的 TCP/IP 模型,Linux 網(wǎng)絡(luò)協(xié)議棧就是按照了該模型來(lái)實(shí)現(xiàn)的。
TCP/IP 模型主要分為應(yīng)用層、傳輸層、網(wǎng)絡(luò)層、網(wǎng)絡(luò)接口層四層,每一層負(fù)責(zé)的職責(zé)都不同,這也是 Linux 網(wǎng)絡(luò)協(xié)議棧主要構(gòu)成部分。
當(dāng)應(yīng)用程序通過(guò) Socket 接口發(fā)送數(shù)據(jù)包,數(shù)據(jù)包會(huì)被網(wǎng)絡(luò)協(xié)議棧從上到下進(jìn)行逐層處理后,才會(huì)被送到網(wǎng)卡隊(duì)列中,隨后由網(wǎng)卡將網(wǎng)絡(luò)包發(fā)送出去。
而在接收網(wǎng)絡(luò)包時(shí),同樣也要先經(jīng)過(guò)網(wǎng)絡(luò)協(xié)議棧從下到上的逐層處理,最后才會(huì)被送到應(yīng)用程序。