之前咸魚在《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
函數實際執行的是 .NET_sendmsg
協議棧函數
這時候內核會去找 socket 上對應的具體協議發送函數
以 TCP 為例,具體協議發送函數為 tcp_sendmsg
tcp_sendmsg
會去申請一個內核態內存 skb(sk_buff)
,然后掛到發送隊列上(發送隊列是由 skb 組成的一個鏈表)
接著把用戶待發送的數據拷貝到 skb 中,拷貝之后會觸發【發送】操作
這里說的發送是指在當前上下文中,待發送數據從 socket 層發送到傳輸層
需要注意的是,這時候不一定開始真正發送,因為還要進行一些條件判斷(比如說發送隊列中的數據已經超過了窗口大小的一半)
只有滿足了條件才能夠發送,如果沒有滿足條件這次系統調用就可能直接返回了
-
傳輸層處理
接著數據來到了傳輸層
傳輸層主要看 tcp_write_xmit
函數,這個函數處理了傳輸層的擁塞控制、滑動窗口相關的工作
該函數會根據發送窗口和最大段大小等因素計算出本次發送的數據大小,然后將數據封裝成 TCP 段并發送出去
如果滿足窗口要求,設置 TCP 頭然后將數據傳到更低的網絡層進行處理
在傳輸層中,內核主要做了兩件事:
-
復制一份數據(skb)
為什么要復制一份出來呢?因為網卡發送完成之后,skb 會被釋放掉,但 TCP 協議是支持丟失重傳的
所以在收到對方的 ACK 之前必須要備份一個 skb 去為重傳做準備
實際上一開始發送的是 skb 的拷貝版,收到了對方的 ACK 之后系統才會把真正的 skb 刪除掉
-
封裝 TCP 頭
系統會根據實際情況添加 TCP 頭封裝成 TCP 段
這里需要知道的是:每個 skb 內部包含了網絡協議中的所有頭部信息,例如 mac 頭、IP 頭、TCP/UDP 頭等
在設置這些頭部時,內核會通過調整指針的位置來填充相應的字段,而不是頻繁申請和拷貝內存
比如說在設置 TCP 頭的時候,只是把指針指向 skb 的合適位置。后面再設置 IP 頭的時候,再把指針挪一挪就行
這種方式利用了 skb 數據結構的鏈表特性可以避免內存分配和數據拷貝所帶來的性能開銷,從而提高數據傳輸的效率
-
網絡層處理
數據離開了傳輸層之后,就來到了網絡層
網絡層主要做下面的事情:
-
路由項查找:
根據目標 IP 地址查找路由表,確定數據包的下一跳(ip_queue_xmit
函數)
-
IP 頭設置:
根據路由表查找的結果,設置 IP 頭中的源和目標 IP 地址、TTL(生存時間)、IP 協議等字段
-
netfilter 過濾:
netfilter 是 Linux 內核中的一個框架,用于實現數據包的過濾和修改
在網絡層,netfilter 可以用于對數據包進行過濾、NAT(網絡地址轉換)等操作
-
skb 切分:
如果數據包的大小超過了 MTU(最大傳輸單元),需要將數據包進行切分成多個片段,以適應網絡傳輸,每個片段會被封裝成單獨的 skb
-
數據鏈路層處理
當數據來到了數據鏈路層之后,會有兩個子系統協同工作,確保數據包在發送和接收過程中能夠正確地對數據進行封裝、解析和傳輸
-
鄰居子系統
管理和維護主機或路由器與其它設備之間的鄰居關系
鄰居子系統里會發送 arp 請求找鄰居,然后把鄰居信息存在鄰居緩存表里,用于存儲目標主機的 MAC 地址
當需要發送數據包到某個目標主機時,數據鏈路層會首先查詢鄰居緩存表,以獲取目標主機的 MAC 地址,從而正確地封裝數據包(封裝 MAC 頭)
-
網絡設備子系統
網絡設備子系統負責處理與物理網絡接口相關的操作,包括數據包的封裝和發送,以及從物理接口接收數據包并進行解析
網絡設備子系統不但處理數據包的格式轉換,如在以太網中添加幀頭和幀尾,以及從幀中提取數據
還負責處理硬件相關的操作,如發送和接收數據包的時鐘同步、物理層錯誤檢測等
-
到達網卡發送隊列
接著網絡設備子系統會選擇一個合適的網卡發送隊列并把 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 系統中發送網絡數據包的流程:
-
傳輸層處理:以 TCP 為例,在傳輸層中會復制一份數據(為了丟失重傳),然后為數據封裝 TCP 頭
-
網絡層處理:選取路由(確認下一跳的 IP)、填充 IP 頭、netfilter 過濾、對超過 MTU 大小的數據包進行分片等操作
-
鄰居子系統和網絡設備子系統處理:在這里數據會被進一步處理和封裝,然后被添加到網卡的發送隊列中
-
應用程序通過 socket 提供的接口進行系統調用,將數據從用戶態拷貝到內核態的 socket 緩沖區中
-
網絡協議棧從 socket 緩沖區中拿取數據,并按照 TCP/IP 協議棧從上到下逐層處理
-
驅動程序從發送隊列中讀取 skb 的描述信息然后掛在 RingBuffer 上,接著將 skb 的描述信息映射到網卡可訪問的內存 DMA 區域中
-
網卡將數據發送到網絡
-
當數據發送完成后觸發硬中斷,釋放 skb 內存和 RingBuffer 內存