Linux 網(wǎng)絡(luò)發(fā)包流程
哈嘍大家好,我是咸魚。
之前咸魚在《Linux 網(wǎng)絡(luò)收包流程》一文中介紹了 Linux 是如何實現(xiàn)網(wǎng)絡(luò)接收數(shù)據(jù)包的。
簡單回顧一下:
- 數(shù)據(jù)到達網(wǎng)卡之后,網(wǎng)卡通過 DMA 將數(shù)據(jù)放到內(nèi)存分配好的一塊 ring buffer 中,然后觸發(fā)硬中斷
- CPU 收到硬中斷之后簡單的處理了一下(分配 skb_buffer),然后觸發(fā)軟中斷
- 軟中斷進程 ksoftirqd 執(zhí)行一系列操作(例如把數(shù)據(jù)幀從 ring ruffer上取下來)然后將數(shù)據(jù)送到三層協(xié)議棧中
- 在三層協(xié)議棧中數(shù)據(jù)被進一步處理發(fā)送到四層協(xié)議棧
- 在四層協(xié)議棧中,數(shù)據(jù)會從內(nèi)核拷貝到用戶空間,供應(yīng)用程序讀取
- 最后被處在應(yīng)用層的應(yīng)用程序去讀取
當(dāng) Linux 要發(fā)送一個數(shù)據(jù)包的時候,這個包是怎么從應(yīng)用程序再到 Linux 的內(nèi)核最后由網(wǎng)卡發(fā)送出去的呢?
那么今天咸魚將會為大家介紹 Linux 是如何實現(xiàn)網(wǎng)絡(luò)發(fā)送數(shù)據(jù)包。
發(fā)包流程
假設(shè)我們的網(wǎng)卡已經(jīng)啟動好(分配和初始化 RingBuffer) 且 server 和 client 已經(jīng)建立好 socket。
這里需要注意的是,網(wǎng)卡在啟動過程中申請分配的 RingBuffer 是有兩個:
- igb_tx_buffer 數(shù)組:這個數(shù)組是內(nèi)核使用的,用于存儲要發(fā)送的數(shù)據(jù)包描述信息,通過 vzalloc申請的
- e1000_adv_tx_desc 數(shù)組:這個數(shù)組是網(wǎng)卡硬件使用的,用于存儲要發(fā)送的數(shù)據(jù)包,網(wǎng)卡硬件可以通過 DMA 直接訪問這塊內(nèi)存,通過 dma_alloc_coherent分配
igb_tx_buffer 數(shù)組中的每個元素都有一個指針指向 e1000_adv_tx_desc;
這樣內(nèi)核就可以把要發(fā)送的數(shù)據(jù)填充到 e1000_adv_tx_desc 數(shù)組上;
然后網(wǎng)卡硬件會直接從 e1000_adv_tx_desc 數(shù)組中讀取實際數(shù)據(jù),并將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上。
拷貝到內(nèi)核
socket 系統(tǒng)調(diào)用將數(shù)據(jù)拷貝到內(nèi)核
應(yīng)用程序首先通過 socket 提供的接口實現(xiàn)系統(tǒng)調(diào)用,我們在用戶態(tài)使用的 send 函數(shù)和 sendto 函數(shù)其實都是 sendto 系統(tǒng)調(diào)用實現(xiàn)的,send/sendto函數(shù) 只是為了用戶方便,封裝出來的一個更易于調(diào)用的方式而已。
在 sendto 系統(tǒng)調(diào)用內(nèi)部,首先 sockfd_lookup_light 函數(shù)會查找與給定文件描述符(fd)關(guān)聯(lián)的 socket;
接著調(diào)用 sock_sendmsg 函數(shù)(sock_sendmsg ==> __sock_sendmsg ==> __sock_sendmsg_nosec);
其中 sock->ops->sendmsg 函數(shù)實際執(zhí)行的是 inet_sendmsg 協(xié)議棧函數(shù):
這時候內(nèi)核會去找 socket 上對應(yīng)的具體協(xié)議發(fā)送函數(shù)。
以 TCP 為例,具體協(xié)議發(fā)送函數(shù)為 tcp_sendmsg:
tcp_sendmsg 會去申請一個內(nèi)核態(tài)內(nèi)存 skb(sk_buff) ,然后掛到發(fā)送隊列上(發(fā)送隊列是由 skb 組成的一個鏈表):
接著把用戶待發(fā)送的數(shù)據(jù)拷貝到 skb 中,拷貝之后會觸發(fā)【發(fā)送】操作,這里說的發(fā)送是指在當(dāng)前上下文中,待發(fā)送數(shù)據(jù)從 socket 層發(fā)送到傳輸層。
需要注意的是,這時候不一定開始真正發(fā)送,因為還要進行一些條件判斷(比如說發(fā)送隊列中的數(shù)據(jù)已經(jīng)超過了窗口大小的一半)。
只有滿足了條件才能夠發(fā)送,如果沒有滿足條件這次系統(tǒng)調(diào)用就可能直接返回了。
網(wǎng)絡(luò)協(xié)議棧處理
傳輸層處理
接著數(shù)據(jù)來到了傳輸層,傳輸層主要看 tcp_write_xmit 函數(shù),這個函數(shù)處理了傳輸層的擁塞控制、滑動窗口相關(guān)的工作,該函數(shù)會根據(jù)發(fā)送窗口和最大段大小等因素計算出本次發(fā)送的數(shù)據(jù)大小,然后將數(shù)據(jù)封裝成 TCP 段并發(fā)送出去,如果滿足窗口要求,設(shè)置 TCP 頭然后將數(shù)據(jù)傳到更低的網(wǎng)絡(luò)層進行處理。
在傳輸層中,內(nèi)核主要做了兩件事:
(1) 復(fù)制一份數(shù)據(jù)(skb)
為什么要復(fù)制一份出來呢?因為網(wǎng)卡發(fā)送完成之后,skb 會被釋放掉,但 TCP 協(xié)議是支持丟失重傳的,所以在收到對方的 ACK 之前必須要備份一個 skb 去為重傳做準(zhǔn)備。
實際上一開始發(fā)送的是 skb 的拷貝版,收到了對方的 ACK 之后系統(tǒng)才會把真正的 skb 刪除掉。
(2) 封裝 TCP 頭
系統(tǒng)會根據(jù)實際情況添加 TCP 頭封裝成 TCP 段。
這里需要知道的是:每個 skb 內(nèi)部包含了網(wǎng)絡(luò)協(xié)議中的所有頭部信息,例如 MAC 頭、IP 頭、TCP/UDP 頭等,在設(shè)置這些頭部時,內(nèi)核會通過調(diào)整指針的位置來填充相應(yīng)的字段,而不是頻繁申請和拷貝內(nèi)存。
比如說在設(shè)置 TCP 頭的時候,只是把指針指向 skb 的合適位置。后面再設(shè)置 IP 頭的時候,再把指針挪一挪就行。
這種方式利用了 skb 數(shù)據(jù)結(jié)構(gòu)的鏈表特性可以避免內(nèi)存分配和數(shù)據(jù)拷貝所帶來的性能開銷,從而提高數(shù)據(jù)傳輸?shù)男省?/p>
網(wǎng)絡(luò)層處理
數(shù)據(jù)離開了傳輸層之后,就來到了網(wǎng)絡(luò)層。
網(wǎng)絡(luò)層主要做下面的事情:
(1) 路由項查找:
根據(jù)目標(biāo) IP 地址查找路由表,確定數(shù)據(jù)包的下一跳(ip_queue_xmit 函數(shù))。
(2) IP 頭設(shè)置:
根據(jù)路由表查找的結(jié)果,設(shè)置 IP 頭中的源和目標(biāo) IP 地址、TTL(生存時間)、IP 協(xié)議等字段。
(3) netfilter 過濾:
netfilter 是 Linux 內(nèi)核中的一個框架,用于實現(xiàn)數(shù)據(jù)包的過濾和修改。
在網(wǎng)絡(luò)層,netfilter 可以用于對數(shù)據(jù)包進行過濾、NAT(網(wǎng)絡(luò)地址轉(zhuǎn)換)等操作。
(4) skb 切分:
如果數(shù)據(jù)包的大小超過了 MTU(最大傳輸單元),需要將數(shù)據(jù)包進行切分成多個片段,以適應(yīng)網(wǎng)絡(luò)傳輸,每個片段會被封裝成單獨的 skb。
數(shù)據(jù)鏈路層處理
當(dāng)數(shù)據(jù)來到了數(shù)據(jù)鏈路層之后,會有兩個子系統(tǒng)協(xié)同工作,確保數(shù)據(jù)包在發(fā)送和接收過程中能夠正確地對數(shù)據(jù)進行封裝、解析和傳輸。
(1) 鄰居子系統(tǒng)
管理和維護主機或路由器與其它設(shè)備之間的鄰居關(guān)系,鄰居子系統(tǒng)里會發(fā)送 arp 請求找鄰居,然后把鄰居信息存在鄰居緩存表里,用于存儲目標(biāo)主機的 MAC 地址。
當(dāng)需要發(fā)送數(shù)據(jù)包到某個目標(biāo)主機時,數(shù)據(jù)鏈路層會首先查詢鄰居緩存表,以獲取目標(biāo)主機的 MAC 地址,從而正確地封裝數(shù)據(jù)包(封裝 MAC 頭)。
(2) 網(wǎng)絡(luò)設(shè)備子系統(tǒng)
網(wǎng)絡(luò)設(shè)備子系統(tǒng)負責(zé)處理與物理網(wǎng)絡(luò)接口相關(guān)的操作,包括數(shù)據(jù)包的封裝和發(fā)送,以及從物理接口接收數(shù)據(jù)包并進行解析。
網(wǎng)絡(luò)設(shè)備子系統(tǒng)不但處理數(shù)據(jù)包的格式轉(zhuǎn)換,如在以太網(wǎng)中添加幀頭和幀尾,以及從幀中提取數(shù)據(jù),還負責(zé)處理硬件相關(guān)的操作,如發(fā)送和接收數(shù)據(jù)包的時鐘同步、物理層錯誤檢測等。
(3) 到達網(wǎng)卡發(fā)送隊列
接著網(wǎng)絡(luò)設(shè)備子系統(tǒng)會選擇一個合適的網(wǎng)卡發(fā)送隊列并把 skb 添加到隊列中(繞過軟中斷處理程序),然后,內(nèi)核會調(diào)用網(wǎng)卡驅(qū)動的入口函數(shù) dev_hard_start_xmit 來觸發(fā)數(shù)據(jù)包的發(fā)送。
在一些情況下,鄰居子系統(tǒng)還會將 skb 數(shù)據(jù)包添加到軟中斷隊列(softnet_data)上,并觸發(fā)軟中斷(NET_TX_SOFTIRQ),這個過程是為了將 skb 數(shù)據(jù)包交給軟中斷處理程序進行進一步處理和發(fā)送。軟中斷處理程序會負責(zé)實際的數(shù)據(jù)包發(fā)送,這就是為什么一般服務(wù)器上查看 /proc/softirqs,一般 NET_RX 都要比 NET_TX 大的多的原因之一。
即對于收包來說,都是要經(jīng)過 NET_RX 軟中斷;而對于發(fā)包來說,只有某些情況下才觸發(fā) NET_TX 軟中斷:
數(shù)據(jù)發(fā)送
驅(qū)動程序從發(fā)送隊列中讀取 skb 的描述信息,將其掛到 RingBuffer 上(前面提到的igb_tx_buffer 數(shù)組)
接著將 skb 的描述信息映射到網(wǎng)卡可訪問的內(nèi)存 DMA 區(qū)域中(前面提到的e1000_adv_tx_desc 數(shù)組)
網(wǎng)卡會直接從 e1000_adv_tx_desc 數(shù)組中根據(jù)描述信息讀取實際數(shù)據(jù)并將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)。這樣就完成了數(shù)據(jù)包的發(fā)送過程
收尾工作
當(dāng)數(shù)據(jù)發(fā)送完成后,網(wǎng)卡設(shè)備會觸發(fā)一個中斷(NET_RX_SOFTIRQ),這個中斷通常稱為“發(fā)送完成中斷”或者“發(fā)送隊列清理中斷”;
這個中斷的主要作用是執(zhí)行發(fā)送完成的清理工作,包括釋放之前為數(shù)據(jù)包分配的內(nèi)存,即釋放 skb 內(nèi)存和 RingBuffer 內(nèi)存;
最后,當(dāng)收到這個 TCP 報文的 ACK 應(yīng)答時,傳輸層就會釋放原始的 skb(前面有講到發(fā)送的其實是 skb 的拷貝版)。
可以看到,當(dāng)數(shù)據(jù)發(fā)送完成以后,通過硬中斷的方式來通知驅(qū)動發(fā)送完畢,而這個中斷類型是 NET_RX_SOFTIRQ。
前面我們講到過網(wǎng)卡收到一個網(wǎng)絡(luò)包的時候,會觸發(fā) NET_RX_SOFTIRQ中斷去告訴 CPU 有數(shù)據(jù)要處理,也就是說,無論是網(wǎng)卡接收一個網(wǎng)絡(luò)包還是發(fā)送網(wǎng)絡(luò)包結(jié)束之后,觸發(fā)的都是 NET_RX_SOFTIRQ。
總結(jié)
最后總結(jié)一下在 Linux 系統(tǒng)中發(fā)送網(wǎng)絡(luò)數(shù)據(jù)包的流程:
最后總結(jié)一下在 Linux 系統(tǒng)中發(fā)送網(wǎng)絡(luò)數(shù)據(jù)包的流程:
(1) 應(yīng)用程序通過 socket 提供的接口進行系統(tǒng)調(diào)用,將數(shù)據(jù)從用戶態(tài)拷貝到內(nèi)核態(tài)的 socket 緩沖區(qū)中
(2) 網(wǎng)絡(luò)協(xié)議棧從 socket 緩沖區(qū)中拿取數(shù)據(jù),并按照 TCP/IP 協(xié)議棧從上到下逐層處理
- 傳輸層處理:以 TCP 為例,在傳輸層中會復(fù)制一份數(shù)據(jù)(為了丟失重傳),然后為數(shù)據(jù)封裝 TCP 頭
- 網(wǎng)絡(luò)層處理:選取路由(確認下一跳的 IP)、填充 IP 頭、netfilter 過濾、對超過 MTU 大小的數(shù)據(jù)包進行分片等操作
- 鄰居子系統(tǒng)和網(wǎng)絡(luò)設(shè)備子系統(tǒng)處理:在這里數(shù)據(jù)會被進一步處理和封裝,然后被添加到網(wǎng)卡的發(fā)送隊列中
(3) 驅(qū)動程序從發(fā)送隊列中讀取 skb 的描述信息然后掛在 RingBuffer 上,接著將 skb 的描述信息映射到網(wǎng)卡可訪問的內(nèi)存 DMA 區(qū)域中
(4) 網(wǎng)卡將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)
(5) 當(dāng)數(shù)據(jù)發(fā)送完成后觸發(fā)硬中斷,釋放 skb 內(nèi)存和 RingBuffer 內(nèi)存