Socket 系統調用深入研究 -TCP 協議的整個通信過程-

內核態和用戶態


通俗的說,用戶空間就是運行着用戶編寫的應用程序的虛擬內存空間。在 32 位的操作系統中,每個進程都有 4GB 獨立的虛擬內存空間,而 0 ~ 3GB 的虛擬內存空間就是用戶空間 。

內核空間就是運行着操作系統代碼的虛擬內存空間,而 3GB ~ 4GB 的虛擬內存空間就是內核空間。

在 Linux 中(當然 windows 下也有用戶態和內核態的區分,不過一般說用戶態和內核態針對的事 Linux 系統而已),爲了更好地保護內核空間,將程序運行空間分爲內核空間和用戶空間(也就是常說的內核態和用戶態),它們分別運行在不同的級別上,邏輯上相互隔離的。因此,用戶進程在通常情況下不允許訪問內核數據,他們只能在用戶空間訪問用戶數據,調用用戶空間的函數。

但是,在有些情況下,用戶空間的進程需要的進程需要獲得一定的系統服務(調用內核空間的程序),這時操作系統就必須調用系統爲用戶提供的 “特殊接口”-系統調用規定用戶進程進入內核空間的具體位置。進行系統調用時,程序運行空間需要從用戶空間進入內核空間,處理完後在返回內核空間。這就涉及到了” 系統調用”。

系統調用


系統調用是操作系統提供給用戶程序調用的一組 “特殊” 編程接口,用戶程序可以通過這組 “特殊” 接口獲得操作系統內核提供的服務。通常,我們可以將這組 “特殊” 的接口稱之爲內核 API.

系統調用按照功能邏輯可以分爲:進程控制,進程間通信,文件系統控制,系統控制,存儲管理,網絡管理,socket 控制,用戶管理等。

用戶編程接口 API


通常,在日常的軟件開發中,系統調用不直接與程序員進行交互,它僅僅是一個軟中斷機制向內核提交請求以獲得內核服務的接口。實際使用中程序員調用的通常是用戶編程接口-API,然後在由用戶編程接口 API 去通知內核完成相應的系統調用。

例如,獲取進程號的 API 函數 getpid() 對應 getpid 系統調用。但並不是所有的函數都對應一個系統調用,有時,一個 API 函數會需要幾個系統調用來共同完成函數的功能,甚至有一些 API 函數不需要相應的系統調用(因此它所完成的不是內核提同的服務)。

在 Linux 中用戶編程接口(API)遵循了在 UNIX 中最流行的應用編程界面標準-POSIX 標準。

這裏博客的主要內容不是介紹 linux 系統的 API,而主要介紹 linux 系統中涉及到網絡 API(也是基於 POSIX 標準的網絡 API)。

詞彙解釋


爲了方便後續的說明,這裏先對涉及到的一些概念進行說明。

通信五元組

服務器與客戶端的通信是通過五元組來區分的,五元組的元素通常是指源 IP 地址,源端口,目的 IP 地址,目的端口和傳輸層協議(TCP/UDP). 五元組能夠區分不同會話,並且對應的會話是唯一的。通過五元組我們就可以實現一次找到一次通信的來源和去處。

TCB 控制塊

TCB 是什麼東西,關於 TCB 的資料不太多,TCB 在整個 TCP 生命週期具體有什麼作用,我也不是很清楚,先來看看 TCB 結構的定義。

struct tcb {
	short	tcb_state;	/* TCP state	TCP狀態(11種,比如LISTEN狀態)		*/
	short	tcb_ostate;	/* output state				*/
	short	tcb_type;	   /* TCP type (SERVER, CLIENT)		*/
	int	tcb_mutex;	   /* tcb mutual exclusion			*/
	short	tcb_code;	/* TCP code for next packet		*/
	short	tcb_flags;	    /* various TCB state flags		*/
	short	tcb_error;	/* return error for user side		*/
	
    //五元組信息
	IPaddr	tcb_rip;	/* remote IP address			*/
	u_short	tcb_rport;	/* remote TCP port			*/
	IPaddr	tcb_lip;	/* local IP address			*/
	u_short	tcb_lport;	/* local TCP port			*/
	struct	netif	*tcb_pni; /* pointer to our interface		*/

	tcpseq	tcb_suna;	   /* send unacked				*/
	tcpseq	tcb_snext;	/* send next				*/
	tcpseq	tcb_slast;	/* sequence of FIN, if TCBF_SNDFIN	*/
	u_long	tcb_swindow;	/* send window size (octets)		*/
	tcpseq	tcb_lwseq;	/* sequence of last window update	*/
	tcpseq	tcb_lwack;	/* ack seq of last window update	*/
	u_int	tcb_cwnd;	/* congestion window size (octets)	*/
	u_int	tcb_ssthresh;	/* slow start threshold (octets)	*/
	u_int	tcb_smss;	/* send max segment size (octets)	*/
	tcpseq	tcb_iss;	/* initial send sequence		*/
	int	tcb_srt;	/* smoothed Round Trip Time		*/
	int	tcb_rtde;	/* Round Trip deviation estimator	*/
	int	tcb_persist;	/* persist timeout value		*/
	int	tcb_keep;	/* keepalive timeout value		*/
	int	tcb_rexmt;	/* retransmit timeout value		*/
	int	tcb_rexmtcount;	/* number of rexmts sent		*/

	tcpseq	tcb_rnext;	/* receive next				*/
	tcpseq	tcb_rupseq;	/* receive urgent pointer		*/
	tcpseq	tcb_supseq;	/* send urgent pointer			*/

	int	tcb_lqsize;	/* listen queue size (SERVERs)		*/
	int	tcb_listenq;	/* listen queue port (SERVERs)		*/
	struct tcb *tcb_pptcb;	/* pointer to parent TCB (for ACCEPT)	*/
	int	tcb_ocsem;	/* open/close semaphore 		*/
	int	tcb_dvnum;	/* TCP slave pseudo device number	*/

	int	tcb_ssema;	/* send semaphore			*/
	u_char	*tcb_sndbuf;	/* send buffer				*/
	u_int	tcb_sbstart;	/* start of valid data			*/
	u_int	tcb_sbcount;	/* data character count			*/
	u_int	tcb_sbsize;	/* send buffer size (bytes)		*/

	int	tcb_rsema;	/* receive semaphore			*/
	u_char	*tcb_rcvbuf;	/* receive buffer (circular)		*/
	u_int	tcb_rbstart;	/* start of valid data			*/
	u_int	tcb_rbcount;	/* data character count	 接收數據長度		*/
	u_int	tcb_rbsize;	/* receive buffer size (bytes) 接收緩衝區大小		*/
	u_int	tcb_rmss;	/* receive max segment size		*/
	tcpseq	tcb_cwin;	/* seq of currently advertised window 記錄了當前窗口可接收的最大報文段序號	*/
	int	tcb_rsegq;	/* segment fragment queue		*/
	tcpseq	tcb_finseq;	/* FIN sequence number, or 0		*/
	tcpseq	tcb_pushseq;	/* PUSH sequence number, or 0		*/
	};

TCB(Transmission Control Block,傳輸控制塊), 基本上網上對於這個說法如下:socket 包含兩個成分,一個是 IP 地址,一個是端口號。同一個設備可以對應一個 IP 端口,但不同的 “水管” 用不同的端口號區分開來,於是同一個設備發送給其他不同設備的信息就不會產生混亂。在同一時刻,設備可能會產生多種數據需要分發給不同的設備,爲了確保數據能夠正確分發,TCP 用一種叫做 TCB,也叫傳輸控制塊的數據結構把發給不同設備的數據封裝起來,我們可以把該結構看做是信封。一個 TCB 數據塊包含了數據發送雙方對應的 socket 信息以及擁有裝載數據的緩衝區。在兩個設備要建立連接發送數據之前,雙方都必須要做一些準備工作,分配內存建立起 TCB 數據塊就是連接建立前必須要做的準備工作。

如果相對 TCB 控制塊結構的每個參數都需要了解,那麼可能需要參考更專業的書籍以及對 TCP 有更深入的學習纔可以,不過可以 tcb_state 字段知道 TCB 具有整個 TCP 過程的整個生命週期,具體的來說就是從 socket 創建到 socket 的關閉整個過程,因此 TCB 的創建是在 socket 函數進行創建的,結束於 close 函數。隨着時間的推移,TCB 結構的各個參數也會發生變化。

TCP 通信流程


在 Linux 系統中,一切皆文件,Socket 也不例外,對 socket 的操作實際上也是對某種特殊文件的讀寫操作。只不過操作的文件在服務器和客戶端各自擁有一個。

內核操作 API


在介紹 socket 系統調用之前,先介紹一些內核 API。

fget_light() 和 fput_light(): 輕量級的文件查找入口。多任務對同一個文件進行操作,所以需要對文件做引用計數。fget_light 在當前進程的 struct files_struct 中根據所謂的用戶空間文件描述符 fd 來獲取文件描述符。另外,根據當前 fs_struct 是否被多各進程共享來判斷是否需要對文件描述符進行加鎖,並將加鎖結果存到一個 int 中返回, fput_light 則根據該結果來判斷是否需要對文件描述符解鎖。

fget_light()/fput_light 是 fget/fput 的變形,不用考慮多進程共享同一個文件表而導致的競爭避免鎖。

fget/fput:指在文件表的引用計數 + 1/-1

sockfd_lookup_light:根據 fd 找到相應的 socket object(內核真正操作的對象)。

so_xxx: 內核相關 socket 操作接口。socket object 操作協議棧的 api 入口。

in_pcballoc(): 分配內核內存,內存名字叫 Internet protocol control block。

in_pcbbind(): 綁定 IN_PCB 到指定的地址,如果不指定地址,那麼會尋找一個可用的端口進行綁定

in_pcblookup(): 指定的端口是否可用。

sbappend(): 追加數據到發送緩衝區。

so->so_proto->pr_usrreq: socket object 操作協議棧的函數

tcp_ursreq(): 是 tcp 協議棧操作的入口函數,支持以下操作類型:PRU_ATTACH,PRU_BIND,PRU_LISTEN, PRU_ACCEPT, PRU_CONNECT, PRU_SHUTDOWN,PRU_ABORT, PRU_DETACH,PRU_SEND,PRU_SENDOOB,PRU_RCVD,PRU_RCVOOB

tcp_newtcpcb():TCP control block 被分配,socket 描述符指向的正是這個 TCP control block。

tcp_attach().

tcp_xxx: tcp_close(), tcp_disconect(),tcp_drop()

pr_xxx: 一套 socket 層和協議棧通信的接口,包括 pr_usrreq(),pr_input(),pr_output(),pr_ctlinput(),pr_ctloutput()。

TCP 系統調用


上圖顯示了 TCP 系統調用在物理鏈路上發出之前進行傳播的各個層。,我們一些列的 API 操作都只是在用戶態 (Process) 進行,套接字層 (Socket layer) 接收進行的任何 TCP 系統調用,驗證 TCP 應用程序傳遞的參數的正確性。這是一個獨立於協議的層,因爲尚未將協議連接到調用中。

套接字層下面是協議層 (Protocol layer),該層包含協議的實際實現(本例中爲 TCP)。當套接字層對協議層進行調用時,將確保對兩個層之間共享的數據結構具有獨佔訪問權限。這樣做是爲了避免任何數據結構損壞。

各種網絡設備驅動程序在接口層 (Interface layer) 運行,該層從物理鏈路接收數據,並向物理鏈路傳輸數據。

每個套接字都有一個套接字隊列,每個接口都有一個用於數據通信的接口隊列。然而,整個協議層只有一個協議隊列,稱爲 IP 輸入隊列。接口層通過這個 IP 輸入隊列向協議層輸入數據。協議層使用各自的接口隊列向接口輸出數據。

握手之前

在客戶端沒有連接到服務器之前,服務器需要做一些初始化工作,以便能夠監聽客戶端的連接,主要包括創建 socket,將 socket 綁定到固定的端口和地址 (可選,可以綁定到本機的任意地址),最後監聽客戶端的連接,涉及到的用戶編程接口 API 包括 socket,bind,listen.

socket

在我們調用 socket 編程接口 API 之後,是在內核創建了一個 socket 對象,並返回對象的引用 fd,通過這個 fd 我們可以操作這個 socket。

socket 在系統調用的表現形式如下:

socket (struct proc *p, struct socket_args *uap, int retval)
struct sock_args
{
int domain,
int type,
int protocol;
};

p 是一個指針,指向進行套接字調用的進程的 proc 結構。

uap 是指向 socket_args 結構的指針,該結構包含在 socket 系統調用中傳遞給進程的參數。

retval 是系統調用的返回值。

socket 系統調用通過分配一個新的描述符來創建一個新的 socket。新描述符將返回給調用進程。任何後續的系統調用都用創建的套接字標識。socket 系統調用還將協議分配給創建的套接字描述符。

domain、type 和 protocol 參數值指定要分配給所創建套接字的族、類型和協議 (即我們調用 socket API 傳遞的參數)。下圖顯示了 socket 系統調用的過程。

一旦從進程中檢索到參數,socket 函數就會調用 socreate 函數。socreate 函數根據進程指定的參數查找指向協議切換 protsw 結構的指針。然後,socreate 函數分配一個新的套接字結構。然後進行協議特定的調用 pr_usrreq,進而切換到與套接字描述符關聯的相應協議特定的請求。pr_usrreq 函數的原型爲:

int  pr_usrreq(struct socket ∗so , int req, struct mbuf  ∗m0 , ∗m1 , ∗m2);

在 pr_usrreq 函數中:

so 是指向套接字結構的指針。

req 的功能是標識請求。本例中爲 PRU_ATTACH。

m0、m1 和 m2 是指向 mbuf 結構的指針。值因請求而異。

pr_usrreq 功能爲大約 16 個請求提供服務。

tcp_usrreq() 函數調用了 tcp_attach(), 以處理 PRU_ATTACH 請求。爲了分配 Internet 協議控制塊 (Internet protocol control block),需要調用 in_pcballoc()。在 in_pcballoc()中,調用了內核的內存分配器函數,該函數將內存分配給 Internet 控制塊。完成所有必要的 Internet 控制塊結構指針初始化之後,該控制返回到 tcp_attach()。

分配新的 TCP 控制塊(TCB), 並調用 tcp_newtcpcb() 進行初始化。它還初始化所有 TCP 定時器變量,控制返回給 tcp_attach()。套接字狀態初始化爲 CLOSED。,tcp_usrreq 函數返回時,套接字描述符將指向套接字的 tcp 控制塊 (TCB)。

Internet 控制塊是雙向的循環鏈表,其指針指向套接字結構,同時套接字結構的 so_pcb 字段指向 Internet 控制塊結構。Internet 控制塊還具有指向 TCP 控制塊的指針。

bind

bind 的系統調用如下,其中的 uap 參數即是我們通過 bind API 傳遞給內核層的參數。

bind (struct proc ∗p, struct bind_args ∗uap, int ∗retval)
   struct bind_args 
   {   int s;
       caddr_t name;
       int namelen;
   };

s 是套接字描述符。

name 是指向包含網絡傳輸地址的緩衝區的指針。

namelen 是緩衝區的大小。

bind 系統調用將本地網絡傳輸地址與套接字相關聯。對於客戶端進程,不需要強制 bind 調用。當客戶端進程發出 connect 系統調用時,內核負責執行隱式綁定。服務器進程在接受連接或開始與客戶端通信之前,通常需要發出顯式 bind 請求。

bind 調用將進程指定的本地地址複製到 mbuf,並調用 sobind,後者則根據請求使用 PRU_BIND 調用 tcp_usrreq()。tcp_usrreq() 中的切換實例調用 in_pcbbind(),後者將本地地址和端口號綁定到套接字。in_pcbbind 函數首先執行一些完整性檢查,以確保不綁定套接字兩次,並且保證其綁定了一個地址 (存在多網卡的情況)。in_pcbbind 負責隱式和顯式綁定。

如果調用 in_pcbbind()中的第二個參數(指向 sockaddr_in 結構的指針)非空,則會發生顯式綁定。否則會發生隱式綁定。在顯式綁定的情況下,將對綁定的 IP 地址執行檢查,並相應地設置套接字選項。

bind 系統調用的流程如下:

如果指定的本地端口爲非零值,則如果綁定位於保留端口上(例如,根據 Berkley 約定,端口號 <1024),則會檢查超級用戶權限。然後調用 in_pcblookup()來查找具有所述本地 IP 地址和本地端口號的控制塊。in_pcblookup()驗證本地地址和端口對是否尚未使用。如果 in_pcbbind()中的第二個參數爲 NULL 或本地端口爲零,則通過檢查找到臨時端口(例如,根據 Berkley 約定,端口號> 1024 且 < 5000)。然後調用 in_pcblookup()來驗證找到的端口是否未使用。

listen

listen (struct proc ∗p, struct listen_args ∗uap, int ∗retval)
struct listen_args
{ int s;
   int backlog;
};

在 listen 系統調用中

s 是套接字描述符。

backlog 是套接字上連接數的隊列限制。

listen 調用指示協議,服務器進程準備接受套接字上任何新傳入的連接。存在一個可以排列的連接數限制,在該連接數之後,忽略任何進一步的連接請求。

關於 backlog 參數後面介紹完三次握手之後還會繼續說明在不同系統中,這個參數的作用。

listen 系統調用使用套接字描述符和 listen 調用中指定的 backlog 值調用 solisten。solisten 僅使用 PRU_LISTEN 作爲請求調用 tcp_usrreq 函數。在 tcp_usrreq() 函數的 switch 語句中,PRU_LISTEN 的實例檢查套接字是否綁定到端口。如果端口爲零,則調用 in_pcbbind(),將套接字綁定到一個端口

如果端口上已經有偵聽套接字,則該套接字的狀態將更改爲 LISTEN。通常,所有服務器進程都會偵聽一個已知的端口號。調用 in_pcbbind 爲服務器進程執行隱式綁定是非常罕見的。listen 系統調用流程如下。

TCP 三次握手

在 TCP 握手的過程中,編程接口包括 connect 和 accept 接口,前者用戶客戶端連接到服務器,後者用於服務器接受客戶端的連接,在具體介紹系統調用之前,先來講一下三次握手,關於三次握手網上也有很多資料。我這裏也是參考了大量的博客,個人覺得比較好的來講解。

當服務器端創建好 socket,並調用了 bind 和 listen 之後,服務器就處於監聽狀態,隨時監聽客戶端的連接,握手過程如下圖所示。

當客戶端調用 connect 函數之後,服務器和客戶端之間就開啓了握手, 在服務器端會維護 2 個連接隊列,一個是半連接隊列(又稱爲 SYN 隊列),另一個是全連接隊列(又稱爲全連接隊列)。第一次握手的時候,客戶端會攜帶客戶端的信息 (地址和端口) 以及服務器的信息 (要連接的服務器地址和端口等) 以及會發送一個同步序號(SYN)給服務器,這個序號告訴服務器需要使用這個序號 + 1 來同我進行同步,服務器接着會創建一個 socket(這個 socket 信息不完整,不能進行通信,不過該 socket 具有 TCB 控制塊信息,能夠存放 TCP 狀態信息),並將這個 socket 信息放入到半連接隊列,此時客戶端的 TCP 狀態爲 SYN_SET, 服務器端的 TCP 狀態爲 SYN_RECV, 服務器隨即給客戶端發送一個 ACK(作爲對客戶端請求的迴應,序號值 ACK=SYN+1,注意這個 SYN 爲客戶端發給服務器的 SYN 序號), 並且服務器自身也會發送一個 SYN 序號給服務器,客戶端收到 ACK 和 SYN 後,比對 ACK 是否自己發送的 SYN+1,如果是,則代表這是對我連接的迴應,此時客戶端的狀態爲 ESTABLISHED, 這時候告知客戶端可以接收到服務器的信息,然而由於在網絡環境比較複雜的情況,客戶端可能會連續發送多次請求。如果只設計成兩次握手的情況,服務端只能一直接收請求,然後返回請求信息,也不知道客戶端是否請求成功。這些過期請求的話就會造成網絡連接的混亂,因此這時候客戶端還需要發送第三次握手通知服務器,此時服務器端的 TCP 狀態也處於 ESTABLISHED。這樣服務器與客戶端就建立起了連接,三次握手完成的時候,服務器剛剛創建的 socket 信息就是一個完整的 socket 信息(能夠和客戶端進行通信),並且將該 socket 信息從半連接隊列中移除,並加入到全連接隊列中,這時候 accept 就會從全隊列中取出一個 socket 信息,這個 socket 信息負責和客戶端進行通信。

connect

connect (struct proc ∗p, struct connect_args ∗uap, int ∗retval);
struct connect_args 
{
    int s;
    caddr_t name;
    int namelen;
};

s 是套接字描述符。

name 是指向服務器端 IP / 端口地址對的緩衝區的指針。

namelen 是緩衝區的長度。

connect 系統調用通常由客戶端進程調用,以連接到服務器進程。如果客戶端進程在啓動連接之前沒有顯式發出 bind 系統調用,則本地套接字上的隱式綁定由堆棧負責。

connect 系統調用將外部地址(連接請求需要發送到的地址)從進程複製到內核,並調用 soconnect()。從 soconnect()返回時,connect()函數會發出一個睡眠,直到協議層將其喚醒,這表明連接已建立 (對於阻塞的 fd 而言,當三次握手的第二次握手完成時,connect 就被喚醒) 或套接字上出現了一些錯誤。soconnect()檢查套接字的有效狀態,並調用 pr_usrreq(),請求時使 PRU_CONNECT。

tcp_usrreq()函數中的 switch case 檢查本地端口與套接字的綁定。如果套接字尚未綁定,則調用 in_pcbbind(),執行隱式綁定。然後調用 in_pcbconnect(),它獲取到目的地的路由,找到必須輸出數據包的接口,並驗證 connect()指定的外部套接字對(IP 地址和端口號)是否唯一。然後,它用外部 IP 地址和端口號更新其 Internet 控制塊,並返回到 PRU_CONNECT case 語句。

tcp_usrreq()調用 soisconnecting(),它將客戶端主機上套接字的狀態設置爲 SYN_SENT。調用函數 tcp_output,將 SYN 數據包輸出到網絡上。控制返回到 connect()函數,該函數將一直休眠,直到協議層被喚醒——這表明連接現在已建立,或者套接字上出現了錯誤。

connect 系統調用流程如下:

accept

accept(struct proc ∗p, struct accept_args ∗uap, int ∗retval);
 struct  accept_args 
{
    int s;
    caddr_t name;
    int ∗anamelen;
};

在 accpet 系統調用中:

s 是套接字描述符。

name 是一個緩衝區(OUT 參數),它包含外部主機的網絡傳輸地址。

anamelen 是名稱緩衝區的大小。

accept 系統調用是等待傳入連接的阻塞調用。處理連接請求後,accept 將返回一個新的套接字描述符。此新套接字已連接到客戶端,而其他套接字仍處於偵聽狀態以接受進一步的連接。

accept 系統調用過程如下:

accept 調用首先驗證參數,並等待連接請求到達。在此之前,該功能會在 while 循環中阻塞。一旦新連接到達,協議層就會喚醒服務器進程。Accept 然後檢查阻塞時可能發生的任何套接字錯誤。如果有任何套接字錯誤,該函數將返回,並通過從隊列中拾取新連接並調用 soaccept 進一步進行操作。在 soaccept()中調用 tcp_usrreq()函數,請求爲 PRU_ACCEPT。tcp_usrreq 函數中的可選系統調用 in_setpeeraddr()可以從協議控制塊複製外部 IP 地址和外部端口號,並將它們返回給服務器進程。

注意:在我們調用 accept 編程接口可以傳遞一個大於 0 的 backlog 參數,這個參數在不同系統下可能具有不同含義,在 mac 系統下,這個參數可能是半連接和全連接的總數,此時可以通過設置這個參數可以對 DDOS 攻擊有一定的作用(因爲這個參數的大小對半連接的數量有限制作用,半連接就是客戶端和服務器的第一次連接),然而在 linux 系統下,這個參數代表全連接隊列的最大數量,因此設置這個參數的大小,對於 DDOS 攻擊無能爲力,此時可以通過設置反向代理的方式來阻止 DDOS 攻擊。

TCP 數據收發

客戶端與服務器建立連接後,服務器與客戶端之間就可以進行數據的收發了,我們在最開始學習網絡編程的時候,以爲調用 send/write 接口,就是將數據從一端發送了給了另一端。或者調用 recv/read 接口就是請求從另一端獲取數據,其實這 2 種想法都是錯誤的。

上圖簡單的表示了 send,recv 等讀寫接口都只作用於用戶態,send 的作用僅僅是將要發送的數據拷貝到內核態的發送緩衝區,內核具體什麼時候發送緩衝區數據我們不知道,對於 recv 而言,我們也僅僅是將內核的接收緩衝區中拷貝數據到用戶態。

注意:這裏有很多知識點,比如零拷貝(sendfile)技術。這些面試的時候有的考官喜歡問。

sendmsg

sendmsg ( struct proc∗p, struct sendmsg_args ∗uap, int retval);
struct sendmsg_args
{    
   int s;
   caddr_t msg;
   int flags;
};

在 send 系統調用中

s 是套接字描述符。

msg 是指向 msghdr 結構的指針。

flags 是控制信息。

有四個系統調用在 n/w 接口上發送數據:write、writev、sendto 和 sendmsg。本文只討論 sendmsg()系統調用。所有四個 send 調用最終都會調用 sosend()。send(進程調用的庫函數)、sendto 和 sendmsg 系統調用只能在套接字描述符上操作,write 和 writev 系統調用可以在任何類型的描述符上操作。

sendmsg 系統調用流程如下:

sendmsg 系統調用將要從進程發送的消息複製到內核空間,並調用 sendit()。在 sendit()中,初始化一個結構,將進程的輸出收集到內核的內存緩衝區中。地址和控制信息也會從進程複製到內核。然後調用 sosend(),它執行四項任務:

(1)根據 sendit()函數傳遞的值初始化各種參數。

(2)驗證套接字的條件和連接的狀態,並確定傳遞消息和報告錯誤所需的空間。

(3)分配內存並從進程中複製數據。

(4)進行特定於協議的調用,將數據發送到網絡。

然後調用 tcp_usrreq(),並根據進程指定的標誌,控制切換到 PRU_SEND 或 PRU_SENDOOB(以發送帶區外數據)。對於 PRU_SENDOOB,發送緩衝區大小可以超過 512 字節,將釋放任何分配的內存並中斷控制。否則,sbappend() 和 tcp_output() 函數由 PRU_SEND 和 PRU_SENDOOB 調用。

sbappend() 在發送緩衝區的末尾添加數據,並且 tcp_output() 將該段發送到接口。

recvmsg

recvmsg(struct proc *p, struct recvmsg_args *uap , int *retval);
struct recvmsg_args
{
 int s,
 struct msghdr *msg,
 int flags,
};

在 receive 系統調用中:

s 是套接字描述符。

msg 是指向 msghdr 結構的指針。

flags 指定控制信息。

有四個系統調用可用於從連接接收數據:read、readv、recvfrom 和 recvmsg。雖然 recv(進程使用的庫函數)、recvfrom 和 recvmsg 只對套接字描述符進行操作,但 read 和 readv 可以對任何類型的描述符進行操作。所有讀取系統調用最終都會調用 soreceive()。

上圖展示了 recv 的系統調用流程,recvmsg()和 recvit()函數初始化各種數組和結構,以將接收到的數據從內核發送到進程。recvit()調用 soreceive(),它將接收到的數據從套接字緩衝區傳輸到接收緩衝區進程。soreceive 函數的作用是執行各種檢查,例如:

(1)是否設置了 MSG_OOB 標誌。

(2)進程是否正在嘗試接收數據。

(3)是否應該在足夠的數據到達之前阻塞。

(4)將讀取的數據傳輸到進程。

(5)檢查數據是否爲帶外數據或常規數據,並進行相應處理。

(6)數據接收完成時通知協議。

當設置 MSG_OOB 標誌或數據接收完成時,soreceive()函數會發出依賴於協議的請求。在接收帶外數據的情況下,協議層檢查不同的條件,以驗證接收到的數據是 OOB,然後將其返回到套接字層。在後一種情況下,協議層調用 tcp_output(),將窗口更新段發送到網絡。這會通知另一端任何可用於接收數據的空間。

TCP 四次揮手

當服務器與客戶端不在需要進行數據的收發時,可以使用 close 編程接口關閉 socket,socket 關閉需要經過四次揮手過程。

四次揮手過程這裏不再描述.

Close

soo_close(struct file ∗fp , struct proc ∗p);

fp 是指向文件結構的指針。

p 是指向調用進程的 proc 結構的指針。

close 系統調用關閉或中止套接字上任何掛起的連接。

soo_close()只調用 so_close()函數,該函數首先檢查要關閉的套接字是否是偵聽套接字(接受傳入連接的套接字)。如果是,則遍歷兩個套接字隊列以檢查是否存在任何掛起的連接。對於每個掛起的連接,都會調用 soabort(),它會發出帶有 PRU_ABORT 的 tcp_usrreq()請求, 可選的系統調用 tcp_drop()會檢查套接字的狀態。

如果狀態是 SYN_RCVD,則通過將狀態設置爲 CLOSED 並調用 tcp_output() 發送 RST 段。tcp_close() 函數然後關閉套接字。tcp_close 函數更新路由度量結構的三個變量,然後釋放套接字持有的資源。

如果狀態是 SYN_RCVD,則通過將狀態設置爲 CLOSED 並調用 tcp_output() 發送 RST 段。tcp_close() 函數然後關閉套接字。tcp_close 函數更新路由度量結構的三個變量,然後釋放套接字持有的資源。

如果套接字不是偵聽套接字,則控制開始使用 soclose(),以檢查是否已存在附加到套接字的控制塊。如果不存在,則 sofree() 釋放套接字。如果存在,則調用具有 PRU_DETACH 的 tcp_usrreq() 將協議與套接字分離。PRU_DETACH 的切換實例調用 tcp_disconnect(),以檢查連接狀態是否爲 ESTABLISHED。如果不是,則 tcp_disconnect() 調用 tcp_close(),以釋放 Internet 控制塊。否則,tcp_disconnect() 檢查延遲時間和延遲套接字選項。如果設置了該選項,並且延遲時間爲零,則調用 tcp_drop()。如果未設置,則調用 tcp_usrclosed(),以設置套接字的狀態,並調用 tcp_output()(如果需要發送 FIN 段)。

close 系統調用如下:

涉及到揮手過程的狀態的一些問題

fin_wait1 和 last_ack 狀態不會存在太久,因爲如果另一方沒有發送 ack 迴應時,到了一定時間會超時重傳,但是 fin_wait_2 的時間可能會存在很長的時間,這是爲什麼呢?

出現這種問題的可能原因是服務器沒有 close(可能在調用 close 之前的業務比較耗時,這時候可以將這些業務放到其他線程或者將 close 放在業務之前),導致 fin_wait_2 會等待較長時間,這時候的解決方法可以設置一定的超時時間,如果沒有收到服務器的 FIN,客戶端可以直接關閉進程.

爲什麼會出現 CLOSING 狀態

CLOSING:這個狀態是一個比較特殊的狀態,也比較少見,正常情況下不會出現,但是當雙方同時都作爲主動的一方。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ufTsU_Vk5uBKfoY7jZJ-nQ