TCP/IP的關鍵特徵
我們如何設計一個數據傳輸協議以便保證數據快速、有序、無誤?TCP/IP正是為了這樣的需求被創造的。下面的幾個特徵用於幫助瞭解什麼是TCP/IP協議(棧)。由於對於TCP來講IP是緊密相關的,我們放到一起介紹。
- 面向連接的(Connection-oriented)
一個tcp connection有兩個端(endpoint),每一個endpoint可以用一個***(ip、port)來表達,所以兩 端的話就可以用(local IP address, local port number, remote IP address, remote port number)*** 來表達。
- 數據是雙向流動的
雙向的傳遞二進制流。
- 按序傳送的
接受者接收數據一定是會按照發送者發送數據的順序的。通過一個32-bit integer做標記。通過ACK來保證可靠性,如果發送者收不到接受者的ACK,則會重新發送。
- 流量控制
發送方會根據接收方提供的的窗口大小來決定如何發送數據,不會超過接收方的緩衝能力。
- 擁塞控制
擁塞窗口(congestion window)區別於receive window,是發送方自己根據包ACK的狀態結合特定的擁塞算法計算出的一個window。它表達的當前的網絡狀態。發送發發送的數據上限受到流量控制和擁塞控制共同的作用。
數據傳送
數據通過網絡協議棧發送,如下圖1。
借用於國外大神的圖(下文也會借用很多,不一一說明了),其表達了數據的流動過程。這裡為了防止大家不認真看,我要強調一下右側黑色方塊表達用戶write的新的數據,而灰色的代表發送緩衝區中已有的數據,大括號圈的灰黑兩塊結合代表了一個TCP報文段。整個過程可以分為三個區域,user、kernel和device,其中user和kernel的部分要吃CPU的。這裡的device就是我們說的網卡(Network Interface Card)。
內核socket關聯了兩個緩衝區:
- 一個發送緩衝區為了數據發送。
- 一個接收緩衝區為了接收數據。
在內核中有一個TCP control block(TCB)關聯到socket。TCB包含了連接需要處理的一系列數據,這裡麵包含了TCP的state(LISTEN, ESTABLISHED, TIME_WAIT),receive window, congestion window, sequence number, resending timer等等。
內核中如果當前的TCP狀態允許數據發送,則一個新的TCP報文段(或者說包)就會被創建。
之後報文段流向IP層。IP層在TCP的報文段上加上IP頭並執行IP路由。IP路由是尋找到達目的IP的下一跳的一個程序。IP層計算完並加上IP頭的checksum之後就會把數據發送到鏈路層。鏈路層通過ARP和下一跳的IP地址查找到下一跳的MAC地址,之後鏈路層把其頭加到數據中。至此主機端數據包完成。之後就是調用網卡驅動了。此時如果有包捕獲程序比如tcpdump或者Wireshark處於運行中,內核會把數據包拷貝給它們一份。
驅動根據硬件廠商定義的協議請求傳送數據。網卡在接到數據傳送請求之後把數據包從主存拷貝到它的存儲空間中,之後把數據打到網線。這時,為了遵從以太網標準,網卡會添加IFG(幀間隔)到數據包以便區分數據包的開始。網卡發送完數據包之後就會產生一個CPU中斷,每一箇中斷都一個特定的中斷號,OS根據中斷號選擇合適的驅動對中斷進行處理(驅動啟動的時候會註冊一個對應中斷號的處理函數)。
數據接收
現在我們來看看是怎麼接收數據的,如圖3。
首先網卡把接收到的數據包寫入到它的內存之中。然後對其進行校驗,通過後發送到主機的主存之中。主存中的buffer是驅動分配好的,驅動會把分配好的buffer描述告訴網卡,如果沒有足夠的buffer接受網卡的數據包,網卡會將數據包丟棄。一旦數據包拷貝到主存完成,網卡會通過中斷告知主機OS。
之後驅動會檢查它是否能處理這個新的包。如果能處理,驅動會把數據包包裝成OS認識的結構(linux sk_buffer)並推送到上層。
鏈路層接收到幀後檢查通過的話會按照協議解幀並推送至IP層。
IP層會在解包之後根據包中包含的IP信息決定推送至上層還是轉發到其他IP。如果判斷需要推送至上層,則會解掉IP包頭並推送至TCP層。
TCP在解報之後會根據其四元組找到對應的TCB,之後通過TCP協議處理這個報文。在接收到報文後,會把報文加到接受報文,之後根據TCP的狀態發送一個ACK給對端。
當然上述過程會受到NAT等等Netfilter的作用,這裡不談了,也沒深研究過。當然為了性能,大牛們方方面面也做了很多努力,比如大到RDMA、DPDK等大的軟硬件技術,小到zero-copy、checksum offload等。
數據結構
下面介紹一下關鍵數據結構sk_buff(skb)。
一個skb就是一個發送緩衝區可發送的數據包。從圖4中可以看到其各個指針。不同層級的數據包頭的添加和刪除、數據包的聯合和分割都是通過控制這些指針來實現的。真正的數據結構可能比這複雜很多,但是基本思路是一致的。
TCP control block
一個TCB代表了一個connection,這裡TCB是一個抽象,linux用tcp_sock這個結構表達。下圖5可以看出tcp_sock和fd、socket之間的關係。
當調用系統調用的時候,OS先找到file結構。對於類unix系統,socket、本地file、device都被抽象成file。因此file擁有最少的信息。對於socket,有其自己的結構關聯到file,tcp_sock也會關聯到socket。tcp_sock只是socket的一類,其他還有諸如inet_sock等支持各種協議的sock。所有TCP相關的信息都在tcp_sock中,比如序號啊,各種窗口等。
發送和接收緩衝區就是sk_buffer的list。dst_entry就是路由的結果,為了避免太頻繁的路由,他們是sock關聯的。dst_entry允許簡單的ARP查找,它也是路由表的一部分。tcp_sock通過對四元組進行hash來索引。
驅動和網卡的交互
這一部分的知識可能是網上最難搜索到的部分,很大一部分原因應該是很少有人關注吧,但是瞭解了這部分知識會讓你更通透。
驅動和網卡之間是異步通信。驅動在請求發送數據之後CPU就去幹別的事情去了。網卡發送完包之後通過中斷通知CPU,CPU再通過驅動程序瞭解到結果。和發送數據一樣,接收數據也是異步的。網卡把數據倒騰到主存之後再通過中斷通知CPU。
因此,預留一些空間來緩存發送和接受的buffer是必要的。大多數情況下,網卡使用環結構,這個環基本上就是一個隊列,它具有固定的條目數,每一個條目存儲一個發送或者接受的數據。條目被順序的輪流使用,可以複用。如下圖6,可以看到數據傳送過程。
驅動接收上層的數據並創建一個網卡可以理解的數據包描述(send descriptor),包含了主存地址和大小。由於網卡只認識物理地址,所以驅動還需將虛擬地址轉換成物理地址,之後把send descriptor放到Tx ring之中。下一步通過通知網卡有新的數據了,之後網卡通過DMA(直接內存訪問)獲取元數據和數據發送出去。發送完之後通過DMA把結果寫回,之後發送中斷通知。
數據的接收和發送反推過程差不多,自己看圖7說話吧;-)。
協議棧buffer和控制流
協議棧中的控制流分為幾個階段。圖8顯示了buffer的發送過程。
首先應用程序創建數據並加入到發送緩衝區。如果緩衝區不足則調用失敗或者阻塞調用線程。因此應用程序向內核灌入數據的速率收到緩衝區大小的限制。
之後TCP創建包並通過傳輸隊列(qdisc)發送給驅動。qdisc是一個FIFO結構並且是固定大小,這個大小可以通過ifconfig命令查看,其中的txqueuelen便是,一般情況下它是千級別的。
在驅動和網卡之間是TX ring。之前提到它是定長的,如果它沒有足夠的空間,那麼當傳輸隊列(qdisc)也滿了之後包就會被drop,就形成了之下而上的反壓。
下圖9表現了buffer接收流。
很容易通過發送流反推。值得注意的是驅動和協議棧之間沒有了隊列,數據是通過poll直接獲取的。如果主機處理的速度沒有網卡接收的快,則Rx ring會滿,就會有包被丟棄。一般情況下丟棄不會是因為TCP連接導致的,因為TCP連接有流量控制,但是UDP是沒有的。可以通過ifconfig命令看到很多信息,比如drop、error等包的數量。
最後
現代的軟硬件TCP/IP協議棧單鏈接發送速率到1~2GiB/s完全沒有任何問題(經過實測)。如果你想探索更優秀的性能,你可以嘗試RMDA等技術,他們通過繞過內核以減少拷貝等方式優化了性能,當然可能依賴硬件。
需要C/C++ Linux服務器架構師學習資料私信“資料”(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享
閱讀更多 編程資源庫 的文章