如果你對Linux是如何實現(xiàn) 對用戶原始的網(wǎng)絡包進行協(xié)議頭封裝與解析,為什么會粘包拆包,期間網(wǎng)絡包經(jīng)歷了哪些緩沖區(qū)、經(jīng)歷了幾次拷貝(CPU、DMA),TCP又是如何實現(xiàn)滑動/擁塞窗口 這幾個話題感興趣的話,不妨看下去吧。
1. Linux發(fā)送HTTP網(wǎng)絡包圖像
圖像解析
寫入套接字緩沖區(qū)(添加TcpHeader)
用戶態(tài)進程通過write()系統(tǒng)調用切到內核態(tài)將用戶進程緩沖區(qū)中的HTTP報文數(shù)據(jù)通過Tcp Process處理程序為HTTP報文添加TcpHeader,并進行CPU copy寫入套接字發(fā)送緩沖區(qū),每個套接字會分別對應一個Send-Q(發(fā)送緩沖區(qū)隊列)、Recv-Q(接收緩沖區(qū)隊列),可以通過ss -nt語句獲取當前的套接字緩沖區(qū)的狀態(tài);
# ss -nt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 1024 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 2048 192.168.183.130:52454 192.168.183.130:14465
......
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 13312 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 14336 192.168.183.130:52454 192.168.183.130:14465
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 14480 192.168.183.130:52454 192.168.183.130:14465
套接字緩沖區(qū)發(fā)送隊列由一個個struct sk_buff 結構體的鏈表組成,其中一個sk_buff數(shù)據(jù)結構對應一個網(wǎng)絡包;這個結構體后面會詳細講,是Linux實現(xiàn)網(wǎng)絡協(xié)議棧的核心數(shù)據(jù)結構。
IP層
接著對TCP包在IP Layer層進行網(wǎng)絡包IpHeader的組裝,并經(jīng)由QDisc(排隊規(guī)則)進行轉發(fā);
數(shù)據(jù)鏈路層/物理層
接著網(wǎng)卡設備通過DMA Engine將內存中RingBuffer的Tx.ring塊中的IP包(sk_buff)copy到網(wǎng)卡自身的內存中,并生成CRC等校驗數(shù)據(jù)形成數(shù)據(jù)鏈路包頭部并進行網(wǎng)絡傳輸。
2. sk_buff數(shù)據(jù)結構解析
通過對sk_buff數(shù)據(jù)結構解析的過程中,我們回答文章頭部的幾個問題,以及窺見Linux中的一些設計思想;
進行協(xié)議頭的增添
我們知道,按照網(wǎng)絡棧的設定,發(fā)送網(wǎng)絡包時,每經(jīng)過一層,都會增加對應協(xié)議層的協(xié)議首部,因此Linux采用在sk_buff中的一個Union結構體進行標識:
struct sk_buff {
union {
struct tcphdr*th; // TCP Header
struct udphdr*uh; // UDP Header
struct icmphdr*icmph; // ICMP Header
struct igmphdr*igmph;
struct iphdr*ipiph; // IPv4 Header
struct ipv6hdr*ipv6h; // IPv6 Header
unsigned char*raw; // MAC Header
} h;
}
結構體中存儲的是指向內存中各種協(xié)議的首部地址的指針,而在發(fā)送數(shù)據(jù)包的過程中,sk_buff中的data指針指向最外層的協(xié)議頭;
網(wǎng)絡包的大小占用
考慮一個包含2bytes的網(wǎng)絡包,需要包括 預留頭(64 bytes) + Mac頭(14bytes) + IP頭(20bytes) + Tcp頭(32bytes) + 有效負載為2bytes(len) + skb_shared_info(320bytes) = 452bytes,向上取整后為512bytes;sk_buff這個存儲結構占用256bytes;則一個2bytes的網(wǎng)絡包需要占用512+256=768bytes(truesize)的內存空間;
因此當發(fā)送這個網(wǎng)絡包時:
-
Case1:不存在緩沖區(qū)積壓,則新建一個sk_buff進行網(wǎng)絡包的發(fā)送;
skb->truesize = 768
skb->datalen = 0 skb_shared_info 結構有效負載 (非線性區(qū)域)
skb->len = 2 有效負載 (線性區(qū)域 + 非線性區(qū)域(datalen),這里暫時不考慮協(xié)議頭部)
-
Case2:如果緩沖區(qū)積壓(存在未被ACK的已經(jīng)發(fā)送的網(wǎng)絡包-即SEND-Q中存在sk_buff結構),Linux會嘗試將當前包合并到SEND-Q的最后一個sk_buff結構中(粘包);考慮我們上述的768bytes的結構體為SEND-Q的最后一個sk_buff,當用戶進程繼續(xù)調用write系統(tǒng)調用寫入2kb的數(shù)據(jù)時,前一個數(shù)據(jù)包還未達到MSS/MTU的限制、整個緩沖區(qū)的大小未達到SO_SENDBUF指定的限制,會進行包的合并,packet data = 2 + 2,頭部的相關信息都可以進行復用,因為套接字緩沖區(qū)與套接字是一一對應的;
tail_skb->truesize = 768
tail_skb->datalen = 0
tail_skb->len = 4 (2 + 2)
發(fā)送窗口
我們在創(chuàng)建套接字的時候,通過SO_SENDBUF指定了發(fā)送緩沖區(qū)的大小,如果設置了大小為2048KB,則Linux在真實創(chuàng)建的時候會設置大小2048*2=4096,因為linux除了要考慮用戶的應用層數(shù)據(jù),還需要考慮linux自身數(shù)據(jù)結構的開銷-協(xié)議頭部、指針、非線性內存區(qū)域結構等...
sk_buff結構中通過sk_wmem_queued標識發(fā)送緩沖區(qū)已經(jīng)使用的內存大小,并在發(fā)包時檢查當前緩沖區(qū)大小是否小于SO_SENDBUF指定的大小,如果不滿足則阻塞當前線程,進行睡眠,等待發(fā)送窗口中有包被ACK后觸發(fā)內存free的回調函數(shù)喚醒后繼續(xù)嘗試發(fā)送;
接收窗口(擁塞窗口)
|<---------- RCV.BUFF ---------------->|
1 2 3
|<-RCV.USER->|<--- RCV.WND ---->|
----|------------|------------------|------|----
RCV.NXT
接收窗口主要分為3部分:
-
RCV.USER 為積壓的已經(jīng)收到但尚未被用戶進程通過read等系統(tǒng)調用獲取的網(wǎng)絡數(shù)據(jù)包;當用戶進程獲取后窗口的左端會向右移動,并觸發(fā)回調函數(shù)將該數(shù)據(jù)包的內存free掉;
-
RCV.WND 為未使用的,推薦返回給該套接字的客戶端發(fā)送方當前剩余的可發(fā)送的bytes數(shù),即擁塞窗口的大??;
-
第三部分為未使用的,尚未預先內存分配的,并不計算在擁塞窗口的大小中;
進入網(wǎng)卡驅動層
NIC (network interface card)在系統(tǒng)啟動過程中會向系統(tǒng)注冊自己的各種信息,系統(tǒng)會分配RingBuffer隊列及一塊專門的內核內存區(qū)用于存放傳輸上來的數(shù)據(jù)包。每個 NIC 對應一個R x.ring 和一個 Tx.ring。一個 RingBuffer 上同一個時刻只有一個 CPU 處理數(shù)據(jù)。
每個網(wǎng)絡包對應的網(wǎng)卡存儲在sk_buff結構的dev_input中;
RingBuffer隊列內存放的是一個個描述符(Descriptor),其有兩種狀態(tài):ready 和 used。
-
初始時 Descriptor 是空的,指向一個空的 sk_buff,處在 ready 狀態(tài)。
-
網(wǎng)卡收到網(wǎng)絡包:當NIC有網(wǎng)絡數(shù)據(jù)包傳入時,DMA負責從NIC取數(shù)據(jù),并在Rx.ring上按順序找到下一個ready的Descriptor,將數(shù)據(jù)存入該 Descriptor指向的sk_buff中,并標記槽為used。
-
網(wǎng)卡發(fā)送網(wǎng)絡包:當sk_buff已經(jīng)在內核空間被寫入完成時,網(wǎng)卡的DMA Engine檢測到Tx.ring有數(shù)據(jù)包完成時,觸發(fā)DMA Copy將數(shù)據(jù)傳輸?shù)骄W(wǎng)卡內存中,并封裝MAC幀。
不同的網(wǎng)絡包發(fā)送函數(shù)有幾次拷貝?
read then write
常見的場景中,當我們要在網(wǎng)絡中發(fā)送一個文件,那么首先需要通過read系統(tǒng)調用陷入內核態(tài)讀取 PageCache 通過CPU Copy數(shù)據(jù)頁到用戶態(tài)內存中,接著將數(shù)據(jù)頁封裝成對應的應用層協(xié)議報文,并通過write系統(tǒng)調用陷入內核態(tài)將應用層報文CPU Copy到套接字緩沖區(qū)中,經(jīng)過TCP/IP處理后形成IP包,最后通過網(wǎng)卡的DMA Engine將RingBuffer Tx.ring中的sk_buff進行DMA Copy到網(wǎng)卡的內存中,并將IP包封裝為幀并對外發(fā)送。
PS:如果PageCache中不存在對應的數(shù)據(jù)頁緩存,則需要通過磁盤DMA Copy到內存中。
因此read then write需要兩次系統(tǒng)調用(4次上下文切換,因為系統(tǒng)調用需要將用戶態(tài)線程切換到內核態(tài)線程進行執(zhí)行),兩次CPU Copy、兩次DMA Copy。
sendFile
用戶線程調用sendFile系統(tǒng)調用陷入內核態(tài),sendFile無需拷貝PageCache中的數(shù)據(jù)頁到用戶態(tài)內存中中,而是通過內核線程將 PageCache 中的數(shù)據(jù)頁直接通過CPU Copy拷貝到套接字緩沖區(qū)中,再經(jīng)由相同的步驟經(jīng)過一次網(wǎng)卡DMA對外傳輸。
因此sendFile需要一次系統(tǒng)調用,一次CPU Copy;
相比于write,sendFile少了一次PageCache拷貝到內存的開銷,但是需要限制在網(wǎng)絡傳輸?shù)氖俏募?,而不是用戶緩沖區(qū)中的匿名頁,并且因為完全在內核態(tài)進行數(shù)據(jù)copy,因此無法添加用戶態(tài)的協(xié)議數(shù)據(jù);
Kafka因為基于操作系統(tǒng)文件系統(tǒng)進行數(shù)據(jù)存儲,并且文件量比較大,因此比較適合通過sendFile進行網(wǎng)絡傳輸?shù)膶崿F(xiàn);
但是sendFile仍然需要一次內核線程的CPU Copy,因此零拷貝更偏向于無需拷貝用戶態(tài)空間中的數(shù)據(jù)。
mmap + write
相比于sendFile直接在內核態(tài)進行文件傳輸,mmap則是通過在進程的虛擬地址空間中映射PageCache,再經(jīng)過write進行網(wǎng)絡寫入;比較適用于小文件的傳輸,因為mmap并沒有立即將數(shù)據(jù)拷貝到用戶態(tài)空間中,所以較大文件會導致頻繁觸發(fā)虛擬內存的 page fault 缺頁異常;
RocketMQ 選擇了 mmap+write 這種零拷貝方式,適用于消息這種小塊文件的數(shù)據(jù)持久化和傳輸。
原文標題:Linux中一個網(wǎng)絡包的發(fā)送/接收流程
文章出處:【微信公眾號:一口Linux】歡迎添加關注!文章轉載請注明出處。
-
Linux
+關注
關注
87文章
11511瀏覽量
213872 -
HTTP
+關注
關注
0文章
525瀏覽量
33540 -
數(shù)據(jù)結構
+關注
關注
3文章
573瀏覽量
40757
原文標題:Linux中一個網(wǎng)絡包的發(fā)送/接收流程
文章出處:【微信號:yikoulinux,微信公眾號:一口Linux】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
深度解析Linux網(wǎng)絡路徑及sk_buff struct 數(shù)據(jù)結構

Linux sk_buff四大指針與相關操作

嵌入式linux TCP/IP協(xié)議棧概述
千兆網(wǎng)絡接口在S3C2440A系統(tǒng)中的應用方案
Linux網(wǎng)絡設備驅動程序
Linux 內核數(shù)據(jù)結構:位圖(Bitmap)
Linux0.11-進程控制塊數(shù)據(jù)結構
網(wǎng)卡的Ring Buffer詳解
網(wǎng)卡的Ring Buffer詳解
Linux如何操作將數(shù)據(jù)包發(fā)送出去

多CPU下的Ring Buffer處理

sk_buff內存空間布局情況與相關操作(一)

sk_buff內存空間布局情況與相關操作(三)

Linux內核中使用的數(shù)據(jù)結構

評論