哈嘍大家好,我是鹹魚 之前鹹魚在《[Linux 網路收包流程](https://mp.weixin.qq.com/s?__biz=MzkzNzI1MzE2Mw==&mid=2247486122&idx=1&sn=df659a7458028772c9595e98d5cefbc1&chksm=c2930 ...
哈嘍大家好,我是鹹魚
之前鹹魚在《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 系統調用 省略了一些代碼 */
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
unsigned int, flags, struct sockaddr __user *, addr,
int, addr_len)
{
...
sock = sockfd_lookup_light(fd, &err, &fput_needed);
...
err = sock_sendmsg(sock, &msg, len);
...
}
在 sendto
系統調用內部,首先 sockfd_lookup_light
函數會查找與給定文件描述符(fd)關聯的 socket
接著調用 sock_sendmsg
函數(sock_sendmsg ==> __sock_sendmsg ==> __sock_sendmsg_nosec
)
其中 sock->ops->sendmsg
函數實際執行的是 inet_sendmsg
協議棧函數
/*
__sock_sendmsg_nosec 函數
iocb:指向與 I/O 操作相關的結構體 kiocb
sock: 指向要執行發送操作的套接字結構體
msg: 指向存儲要發送數據的消息頭結構體 msghdr
size: 要發送的數據大小
*/
static inline int __sock_sendmsg_nosec(struct kiocb *iocb, struct socket *sock,
struct msghdr *msg, size_t size)
{
...
return sock->ops->sendmsg(iocb, sock, msg, size);
}
這時候內核會去找 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 系統中發送網路數據包的流程:
- 應用程式通過 socket 提供的介面進行系統調用,將數據從用戶態拷貝到內核態的 socket 緩衝區中
- 網路協議棧從 socket 緩衝區中拿取數據,並按照 TCP/IP 協議棧從上到下逐層處理
- 傳輸層處理:以 TCP 為例,在傳輸層中會複製一份數據(為了丟失重傳),然後為數據封裝 TCP 頭
- 網路層處理:選取路由(確認下一跳的 IP)、填充 IP 頭、netfilter 過濾、對超過 MTU 大小的數據包進行分片等操作
- 鄰居子系統和網路設備子系統處理:在這裡數據會被進一步處理和封裝,然後被添加到網卡的發送隊列中
- 驅動程式從發送隊列中讀取 skb 的描述信息然後掛在 RingBuffer 上,接著將 skb 的描述信息映射到網卡可訪問的記憶體 DMA 區域中
- 網卡將數據發送到網路
- 當數據發送完成後觸發硬中斷,釋放 skb 記憶體和 RingBuffer 記憶體