TCP/IP 協議棧在 Linux 內核中的 運行時序分析

調研要求

1.在深入理解Linux內核任務調度(中斷處理、softirg、tasklet、wq、內核線程等)機制的基礎上,分析梳理send和recv過程中TCP/IP協議棧相關的運行任務實體及相互協作的時序分析。

2.編譯、部署、運行、測評、原理、源代碼分析、跟蹤調試等。

3.應該包括時序圖。

1.Linux概述

1.1Linux操作系統架構簡介 

Linux系統一般有4個主要部分:內核、shell、文件系統和應用程序。內核、shell和文件系統一起形成了基本的操作系統結構,它們使得用戶可以運行程序、管理文件並使用系統。

內核是操作系統的核心,具有很多最基本功能,如虛擬內存、多任務、共享庫、需求加載、可執行程序和TCP/IP網絡功能。Linux內核的模塊分爲以下幾個部分:存儲管理、CPU和進程管理、文件系統、設備管理和驅動、網絡通信、中斷處理、系統的初始化和系統調用等。而本次調研內容就是Linux5.4.34內核中的下圖所示的藍色部分:TCP/IP協議棧。

 

1.2協議棧簡介

一個主流的清晰的網絡分層模型是OSI七層模型。如下圖所示,它自上而下由應用層,表示層,會話層,傳輸層,網絡層,數據鏈路層,物理層組成。但是這種分層只適用於學習,而不適用於實踐。

 

 

生活中更多的是使用TCP/IP網絡分層模型,這種模型分爲四層,分別爲應用層,傳輸層,網絡接口層,接口層。 

1.3 Linux內核協議棧的實現

每一類軟件模型都不能獨立存在,必定依託系統其他模塊纔可以工作,協議棧也是如此,Linux協議棧是在內核中實現的,具體支持方式如圖所示。在用戶空間最上層的APP就是應用程序,這些應用可以使用glibc庫,這個庫裏封裝了socket,bind,recv,send等函數,這些函數會使用相應的系統調用。然後是INET模塊,它並不是TCP/IP體系必須的一部分,但是TCP/IP層的接口都要通過這個模塊纔可以訪問操縱,這一操作也是在網絡初始化的時候就已經註冊到socket層的。再往下就是整個TCP/IP協議棧的部分,由TCP、IP、Routing System、ICMP、 ARP、Driver模塊組成。

2.sendrecv應用層流程

2.1本次調研採取的測試代碼

服務器端代碼:

 

 

客戶端代碼:

 

 

可以簡化爲下圖所示:

客戶端和服務器端交互具體流程如下:

 

2.2 socket

 應用層的各種網絡應用程序基本上都是通過 Linux Socket 編程接口來和內核空間的網絡協議棧通信的。Linux Socket 是Linux 操作系統的重要組成部分之一,是網絡應用程序的基礎。從層次上來說,它位於應用層,位於傳輸層協議之上,屏蔽了不同網絡協議之間的差異,同時也是網絡編程的入口,它提供了大量的系統調用,構成了網絡程序的主體。

Linux系統中,socket 屬於文件系統的一部分,網絡通信可以被看作是對文件的讀取,使得對網絡的控制和對文件的控制一樣方便。

 

2.2.1 socket的創建 

在內核中與socket對應的系統調用是sys_soceket,所謂的創建套接口,就是在sockfs這個文件系統中創建一個節點,從Linux/Unix的角度來看,該節點是一個文件,不過這個文件具有非普通文件的屬性,於是起了--個獨特的名字socket。由於sockfs文件系統是系統初始化時就保存在全局指針sock_mnt中的,所以申請一個inode的過程便以sock_mnt爲參數。

socket函數本身,經過glibc庫對其封裝,它將通過int 0x80產生-一個軟件中斷(注意不是軟中斷),由內核導向執行sys_socket,基本上參數會原封不動地傳入內核,它們分別是(1) int family,(2) int type, (3) int protocol。 

其調用樹如下圖:

 

sock_create中會繼續調用sock_alloc,該函數創建了struct socket{}結構。

 

通過調用樹可知sock_alloc的作用就是分配和初始化屬於網絡類型的inode。

inode結構中的大部分字段只是對真正的文件系統重要,只有一部分由socket使用。Socket_alloc類型的Inode結構如下:

 

這裏socket socket 部分就是特定文件的數據。Socket{}結構就表示這是一個和網絡有關的文件描述符,是INET層和應用層打開的文件描述一一對應的實體,每一次調用socket都會在INET中保存這麼一個實體。

2.2.2 socket的發送

在創建完socket之後,應用程序會使用send發送數據。在用戶態調用系統接口send、sendto或者sendmsg都是在調用sock_sendmsg。其中send調用的是sendto,只是參數略有不同。

 

 這裏定義了一個struct msghdr msg,他是用來表示要發送的數據的一些屬性。 

 

還有一個struct iovec,他被稱爲io向量,故名思意,用來表示io數據的一些信息。

 

所以,__sys_sendto函數其實做了3件事:

1.通過fd獲取了對應的struct socket

2.創建了用來描述要發送的數據的結構體struct msghdr。

3.調用了sock_sendmsg來執行實際的發送。

 

 

繼續追蹤這個函數,會看到最終調用的是inet_sendmsg

 

這裏間接調用了tcp_sendmsg即傳送到傳輸層。

調試驗證:

2.2.3 socket的接收

對於recv函數,與send類似,自然也是recvfrom的特殊情況,調用的也就是__sys_recvfrom,整個函數的調用路徑與send非常類似:

 

 

 

sock->ops->recvmsg即inet_recvmsg,最後在inet_recvmsg中調用的是tcp_recvmsg

 

調試驗證:

 

3.sendrecv傳輸層流程

3.1 tcp發送數據

 

tcp_sendmsg實際上調用的是int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)

 

 

在tcp_sendmsg_locked中,完成的是將所有的數據組織成發送隊列,這個發送隊列是struct sock結構中的一個域sk_write_queue,這個隊列的每一個元素是一個skb,裏面存放的就是待發送的數據。然後調用了tcp_push()函數。

 

tcp協議的頭部有幾個標誌字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push中會判斷這個skb的元素是否需要push,如果需要就將tcp頭部字段的push置一,置一的過程如下:

 

 

然後,tcp_push調用了__tcp_push_pending_frames(sk, mss_now, nonagle);函數發送數據:

 

隨後又調用了tcp_write_xmit來發送數據:

 

若發送隊列未滿,則準備發送報文

 

檢查發送窗口的大小

 

tcp_write_xmit位於tcpoutput.c中,它實現了tcp的擁塞控制,然後調用了tcp_transmit_skb(sk, skb, 1, gfp)傳輸數據,實際上調用的是__tcp_transmit_skb

 

 

構建TCP頭部和校驗和

 

 

 

tcp_transmit_skbtcp發送數據位於傳輸層的最後一步,這裏首先對TCP數據段的頭部進行了處理,然後調用了網絡層提供的發送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);實現了數據的發送,自此,數據離開了傳輸層,傳輸層的任務也就結束了。

調試驗證:

 

3.2 tcp接收數據

接收函數比發送函數要複雜得多,因爲數據接收不僅僅只是接收,tcp的三次握手也是在接收函數實現的,所以收到數據後要判斷當前的狀態,是否正在建立連接等,根據發來的信息考慮狀態是否要改變,在這裏僅僅考慮在連接建立後數據的接收。

首先從上向下分析,即上一層中調用了tcp_recvmsg。

該函數完成從接收隊列中讀取數據複製到用戶空間的任務;函數在執行過程中會鎖定控制塊,避免軟中斷在tcp層的影響;函數會涉及從接收隊列receive_queue和後備隊列backlog中讀取數據;其中從backlog中讀取的數據,還需要經過sk_backlog_rcv回調,該回調的實現爲tcp_v4_do_rcv,實際上是先緩存到隊列中,然後需要讀取的時候,才進入協議棧處理,此時,是在進程上下文執行的,因爲會設置tp->ucopy.task=current,在協議棧處理過程中,會直接將數據複製到用戶空間。

這裏的copied表示已經複製了多少字節,target表示目標是多少字節。

 

 

 

在連接建立後,若沒有數據到來,接收隊列爲空,進程會在sk_busy_loop函數內循環等待。Lock_sock()傳輸層上鎖,避免軟中斷影響 。

 

 

獲得數據後,遍歷接收隊列,找到滿足讀取的skb

 

並調用函數skb_copy_datagram_msg將接收到的數據拷貝到用戶態,實際調用的是__skb_datagram_iter,這裏同樣用了struct msghdr *msg來實現。

 

 

以上是對數據進行拷貝。

 

如果copied>0,即讀取到數據則繼續,否則的話,也就是沒有讀到想要的數據,[當設置了nonblock時,(表現在timeo=0],就返回-EAGAIN,也就是非阻塞方式。

 

如果目標數據讀取完,則處理後備隊列。但是如果沒有設置nonblock,同時也沒有出現copied >= target的情況,也就是沒有讀到足夠多的數據,則調用sk_wait_data將當前進程等待。也就是我們希望的阻塞方式。阻塞函數sk_wait_data所做的事情就是讓出CPU,等數據來了或者設定超時之後再恢復運行。

然後從下向上分析,即tcp層是如何接收來自ip的數據並且插入相應隊列的。

tcp_v4_rcv函數爲TCP的總入口,數據包從IP層傳遞上來,進入該函數;其協議操作函數結構如下所示,其中handler即爲IP層向TCP傳遞數據包的回調函數,設置爲tcp_v4_rcv;

static struct net_protocol tcp_protocol = {

    .early_demux    =    tcp_v4_early_demux,

    .early_demux_handler =  tcp_v4_early_demux,

    .handler    =    tcp_v4_rcv,

    .err_handler    =    tcp_v4_err,

    .no_policy    =    1,

    .netns_ok    =    1,

    .icmp_strict_tag_validation = 1,

};

IP層處理本地數據包時,會獲取到上述結構的實例,並且調用實例的handler回調,也就是調用了tcp_v4_rcv;

tcp_v4_rcv函數只要做以下幾個工作:(1) 設置TCP_CB (2) 查找控制塊  (3)根據控制塊狀態做不同處理,包括TCP_TIME_WAIT狀態處理,TCP_NEW_SYN_RECV狀態處理,TCP_LISTEN狀態處理 (4) 接收TCP段;

 

 

tcp_v4_rcv判斷狀態爲listen時會直接調用tcp_v4_do_rcv;如果是其他狀態,將TCP包投遞到目的套接字進行接收處理。如果套接字未被上鎖則調用tcp_v4_do_rcv。當套接字正被用戶鎖定,TCP包將暫時排入該套接字的後備隊列(sk_add_backlog)。

 

Tcp_v4_do_ecv檢查狀態如果是established,就調用tcp_rcv_established函數。

 

tcp_rcv_established用於處理已連接狀態下的輸入,處理過程根據首部預測字段分爲快速路徑和慢速路徑。

 

在快路中,若無數據,則處理輸入ack,釋放該skb,檢查是否有數據發送,有則發送;

 

 若有數據,則使用tcp_queue_rcv()將數據加入到接收隊列中。

 

加入方式包括合併到已有數據段,或者加入隊列尾部。

 

回到快路中繼續進行tcp_ack()處理ack , tcp_data_snd_check(sk)檢查是否有數據要發送,需要則發送,__tcp_ack_snd_check(sk, 0)檢查是否有ack要發送,需要則發送.

kfree_skb_partial(skb, fragstolen) skb已經複製到用戶空間,則釋放之。

             

喚醒用戶進程通知有數據可讀。

 

在慢路中,會進行更詳細的校驗,然後處理ack,處理緊急數據,接收數據段。

 

其中數據段可能包含亂序的情況,如果他是有序的就調用tcp_queue_rcv()將數據加入到接收隊列中,無序的就放入無序隊列中tcp_ofo_queue。最後tcp_data_ready喚醒用戶進程通知有數據可讀。

 

調試驗證:

 

 

4.sendrecv網絡層流程

4.1 ip發送數據

ip_queue_xmit是ip層提供給tcp層發送回調,大多數tcp發送都會使用這個回調,tcp層使用tcp_transmit_skb封裝了tcp頭之後調用該函數。

 

Ip_queue_xmit實際上是調用__ip_queue_xmit。

 

Skb_rtable(skb)獲取skb中的路由緩存,然後判斷是否有緩存,如果有緩存就直接進行packet_routed。

 

如果沒有路由緩存就ip_route_output_ports查找路由緩存,在之後封裝ip頭和ip選項的功能。

 

最後調用ip_local_out發送數據包

 

調用__ip_local_out。

 

經過netfilter的LOCAL_OUT鉤子點進行檢查過濾,如果通過,則調用dst_output函數,實際上調用的是ip數據包輸出函數ip_output。

 

裏面調用ip_finish_output。

 

實際上調用的是__ip_finish_output,如果需要分片就調用ip_fragment,否則直接調用ip_finish_output2。

 

在構造好ip頭,檢查完分片之後,會調用鄰居子系統的輸出函數neigh_output進行輸出。

 

 

輸出分爲有二層頭緩存和沒有兩種情況,有緩存時調用neigh_hh_output進行快速輸出,沒有緩存時,則調用鄰居子系統的輸出回調函數進行慢速輸出。

 

最後調用dev_queue_xmit向鏈路層發送數據包。

 

調試驗證:

 

 

4.2 ip接收數據

IP 層的入口函數在 ip_rcv 函數。

 

然後調用已經註冊的 Pre-routing netfilter hook ,完成後最終到達 ip_rcv_finish 函數。

 

如果是發到本機就調用dst_input,裏面由ip_local_deliver函數。

 

判斷是否分片,如果有分片就ip_defrag()進行合併多個數據包的操作,沒有分片就調用ip_local_deliver_finish()。

 

進一步調用ip_protocol_deliver_rcu,該函數根據 package 的下一個處理層的 protocal number,調用下一層接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP)。對於 TCP 來說,函數 tcp_v4_rcv 函數會被調用,從而處理流程進入 TCP 棧。

 

 

調試驗證:

 

 

5.sendrecv鏈路層和物理層流程

5.1 發送數據

上層調用dev_queue_xmit進入鏈路層的處理流程,實際上調用的是__dev_queue_xmit

 

 

調用dev_hard_start_xmit

 

然後調用 xmit_one

 

調用netdev_start_xmit,實際上是調用__netdev_start_xmit

 

調用各網絡設備實現的ndo_start_xmit回調函數指針,其爲數據結構struct net_device,從而把數據發送給網卡,物理層在收到發送請求之後,通過 DMA 將該主存中的數據拷貝至內部RAM(buffer)之中。在數據拷貝中,同時加入符合以太網協議的相關header,IFG、前導符和CRC。對於以太網網絡,物理層發送採用CSMA/CD,即在發送過程中偵聽鏈路衝突。

一旦網卡完成報文發送,將產生中斷通知CPU,然後驅動層中的中斷處理程序就可以刪除保存的 skb 了。

調試驗證:

 

5.2 接受數據

這層的數據接收要涉及到一些中斷和硬件層面的東西。

1: 數據包從外面的網絡進入物理網卡。如果目的地址不是該網卡,且該網卡沒有開啓混雜模式,該包會被網卡丟棄。

2: 網卡將數據包通過DMA的方式寫入到指定的內存地址,該地址由網卡驅動分配並初始化。注: 老的網卡可能不支持DMA,不過新的網卡一般都支持。

3: 網卡通過硬件中斷(IRQ)通知CPU,告訴它有數據來了

4: CPU根據中斷表,調用已經註冊的中斷函數,這個中斷函數會調到驅動程序(NIC Driver)中相應的函數

5: 驅動先禁用網卡的中斷,表示驅動程序已經知道內存中有數據了,告訴網卡下次再收到數據包直接寫內存就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。

6: 啓動軟中斷。這步結束後,硬件中斷處理函數就結束返回了。由於硬中斷處理程序執行的過程中不能被中斷,所以如果它執行時間過長,會導致CPU沒法響應其它硬件的中斷,於是內核引入軟中斷,這樣可以將硬中斷處理函數中耗時的部分移到軟中斷處理函數裏面來慢慢處理。

 

軟中斷會觸發內核網絡模塊中的軟中斷處理函數,內核中的ksoftirqd進程專門負責軟中斷的處理,當它收到軟中斷後,就會調用相應軟中斷所對應的處理函數,對於上面第6步中是網卡驅動模塊拋出的軟中斷,ksoftirqd會調用網絡模塊的net_rx_action函數。

 

 net_rx_action調用網卡驅動裏的naqi_poll函數來一個一個的處理數據包。在poll函數中,驅動會一個接一個的讀取網卡寫到內存中的數據包,內存中數據包的格式只有驅動知道。驅動程序將內存中的數據包轉換成內核網絡模塊能識別的skb格式,然後調用napi_gro_receive函數。

 

napi_gro_receive會直接調用netif_receive_skb_core。

 

netif_receive_skb_core調用 __netif_receive_skb_one_core,將數據包交給上層ip_rcv進行處理。

 

待內存中的所有數據包被處理完成後(即poll函數執行完成),啓用網卡的硬中斷,這樣下次網卡再收到數據的時候就會通知CPU。

 

調試驗證:

 

 

6.時序圖展示

 

 

[1]  https://www.cnblogs.com/sammyliu/p/5225623.html

[2]  https://www.cnblogs.com/myguaiguai/p/12069485.html

[3]  https://blog.csdn.net/weixin_43414275/article/details/106425587

[4]  https://www.cnblogs.com/wanpengcoder/p/11752173.html

[5]  電子書《linux內核協議棧源碼解析》

[6]  源代碼的編譯和運行根據課上ppt進行操作

[7]  報告中所用部分圖和時序圖使用app.diagrams製作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章