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