這篇文章將試圖說明應用程序如何接收網絡上發送過來的TCP消息流,因爲篇幅所限,暫時忽略ACK報文的回覆和接收窗口的滑動。
爲了快速掌握本文所要表達的思想,咱們能夠帶着如下問題閱讀:
一、應用程序調用read、recv等方法時,socket套接字能夠設置爲阻塞或者非阻塞,這兩種方式是如何工做的?
二、若socket爲默認的阻塞套接字,此時recv方法傳入的len參數,是表示必須超時(SO_RCVTIMEO)或者接收到len長度的消息,recv方法纔會返回嗎?並且,socket上能夠設置一個屬性叫作SO_RCVLOWAT,它會與len產生什麼樣的交集,又是決定recv等接收方法何時返回?
三、應用程序開始收取TCP消息,與程序所在的機器網卡上接收到網絡裏發來的TCP消息,這是兩個獨立的流程。它們之間是如何互相影響的?例如,應用程序正在收取消息時,內核經過網卡又在這條TCP鏈接上收到消息時,到底是如何處理的?若應用程序沒有調用read或者recv時,內核收到TCP鏈接上的消息後又是怎樣處理的?
四、recv這樣的接收方法還能夠傳入各類flags,例如MSG_WAITALL、MSG_PEEK、MSG_TRUNK等等。它們是如何工做的?
五、1個socket套接字可能被多個進程在使用,出現併發訪問時,內核是怎麼處理這種情況的?
六、linux的sysctl系統參數中,有相似tcp_low_latency這樣的開關,默認爲0或者配置爲1時是如何影響TCP消息處理流程的?
書接上文。本文將經過三幅圖講述三種典型的接收TCP消息場景,理清內核爲實現TCP消息的接收所實現的4個隊列容器。固然,瞭解內核的實現並非目的,而是如何使用socket接口、如何配置操做系統內核參數,才能使TCP傳輸消息更高效,這纔是最終目的。
不少同窗不但願被內核代碼擾亂了思惟,如何閱讀本文呢?
我會在圖1的步驟都介紹完了纔來從代碼上說明tcp_v4_rcv等主要方法。像flags參數、非阻塞套接字會產生怎樣的效果我是在代碼介紹中說的。而後我會介紹圖二、圖3,介紹它們的步驟時我會穿插一些上文沒有涉及的少許代碼。不喜歡瞭解內核代碼的同窗請直接看完圖1的步驟後,請跳到圖二、圖3中,我認爲這3幅圖覆蓋了主要的TCP接收場景,可以幫助你理清其流程。
接收消息時調用的系統方法要比上一篇發送TCP消息複雜許多。接收TCP消息的過程能夠一分爲二:首先是PC上的網卡接收到網線傳來的報文,經過軟中斷內核拿到而且解析其爲TCP報文,而後TCP模塊決定如何處理這個TCP報文。其次,用戶進程調用read、recv等方法獲取TCP消息,則是將內核已經從網卡上收到的消息流拷貝到用戶進程裏的內存中。
第一幅圖描述的場景是,TCP鏈接上將要收到的消息序號是S1(TCP上的每一個報文都有序號,詳見《TCP/IP協議詳解》),此時操做系統內核依次收到了序號S1-S2的報文、S3-S四、S2-S3的報文,注意後兩個包亂序了。以後,用戶進程分配了一段len大小的內存用於接收TCP消息,此時,len是大於S4-S1的。另外,用戶進程始終沒有對這個socket設置過SO_RCVLOWAT參數,所以,接收閥值SO_RCVLOWAT使用默認值1。另外,系統參數tcp_low_latency設置爲0,即從操做系統的整體效率出發,使用prequeue隊列提高吞吐量。固然,因爲用戶進程收消息時,並無新包來臨,因此此圖中prequeue隊列始終爲空。先不細表。
圖1以下:
上圖中有13個步驟,應用進程使用了阻塞套接字,調用recv等方法時flag標誌位爲0,用戶進程讀取套接字時沒有發生進程睡眠。內核在處理接收到的TCP報文時使用了4個隊列容器(當鏈表理解也可),分別爲receive、out_of_order、prequeue、backlog隊列,本文會說明它們存在的意義。下面詳細說明這13個步驟。
一、當網卡接收到報文並判斷爲TCP協議後,將會調用到內核的tcp_v4_rcv方法。此時,這個TCP鏈接上須要接收的下一個報文序號剛好就是S1,而這一步裏,網卡上收到了S1-S2的報文,因此,tcp_v4_rcv方法會把這個報文直接插入到receive隊列中。
注意:receive隊列是容許用戶進程直接讀取的,它是將已經接收到的TCP報文,去除了TCP頭部、排好序放入的、用戶進程能夠直接按序讀取的隊列。因爲socket不在進程上下文中(也就是沒有進程在讀socket),因爲咱們須要S1序號的報文,而剛好收到了S1-S2報文,所以,它進入了receive隊列。
二、接着,咱們收到了S3-S4報文。在第1步結束後,這時咱們須要收到的是S2序號,但到來的報文倒是S3打頭的,怎麼辦呢?進入out_of_order隊列!從這個隊列名稱就能夠看出來,全部亂序的報文都會暫時放在這。
三、仍然沒有進入來讀取socket,但又過來了咱們指望的S2-S3報文,它會像第1步同樣,直接進入receive隊列。不一樣的時,因爲此時out_of_order隊列不像第1步是空的,因此,引起了接來的第4步。
四、每次向receive隊列插入報文時都會檢查out_of_order隊列。因爲收到S2-S3報文後,期待的序號成爲了S3,這樣,out_of_order隊列裏的惟一報文S3-S4報文將會移出本隊列而插入到receive隊列中(這件事由tcp_ofo_queue方法完成)。
五、終於有用戶進程開始讀取socket了。作過應用端編程的同窗都知道,先要在進程裏分配一塊內存,接着調用read或者recv等方法,把內存的首地址和內存長度傳入,再把創建好鏈接的socket也傳入。固然,對這個socket還能夠配置其屬性。這裏,假定沒有設置任何屬性,都使用默認值,所以,此時socket是阻塞式,它的SO_RCVLOWAT是默認的1。固然,recv這樣的方法還會接收一個flag參數,它能夠設置爲MSG_WAITALL、MSG_PEEK、MSG_TRUNK等等,這裏咱們假定爲最經常使用的0。進程調用了recv方法。
六、不管是何種接口,C庫和內核通過層層封裝,接收TCP消息最終必定會走到tcp_recvmsg方法。下面介紹代碼細節時,它會是重點。
七、在tcp_recvmsg方法裏,會首先鎖住socket。爲何呢?所以socket是能夠被多進程同時使用的,同時,內核中斷也會操做它,而下面的代碼都是核心的、操做數據的、有狀態的代碼,不能夠被重入的,鎖住後,再有用戶進程進來時拿不到鎖就要休眠在這了。內核中斷看到被鎖住後也會作不一樣的處理,參見圖二、圖3。
八、此時,第1-4步已經爲receive隊列裏準備好了3個報文。最上面的報文是S1-S2,將它拷貝到用戶態內存中。因爲第5步flag參數並無攜帶MSG_PEEK這樣的標誌位,所以,再將S1-S2報文從receive隊列的頭部移除,從內核態釋放掉。反之,MSG_PEEK標誌位會致使receive隊列不會刪除報文。因此,MSG_PEEK主要用於多進程讀取同一套接字的情形。
九、如第8步,拷貝S2-S3報文到用戶態內存中。固然,執行拷貝前都會檢查用戶態內存的剩餘空間是否足以放下當前這個報文,不足以時會直接返回已經拷貝的字節數。
十、同上。
十一、receive隊列爲空了,此時會先來檢查SO_RCVLOWAT這個閥值。若是已經拷貝的字節數到如今還小於它,那麼可能致使進程會休眠,等待拷貝更多的數據。第5步已經說明過了,socket套接字使用的默認的SO_RCVLOWAT,也就是1,這代表,只要讀取到報文了,就認爲能夠返回了。
作完這個檢查了,再檢查backlog隊列。backlog隊列是進程正在拷貝數據時,網卡收到的報文會進這個隊列。此時若backlog隊列有數據,就順帶處理下。圖3會覆蓋這種場景。
十二、在本圖對應的場景中,backlog隊列是沒有數據的,已經拷貝的字節數爲S4-S1,它是大於1的,所以,釋放第7步里加的鎖,準備返回用戶態了。
1三、用戶進程代碼開始執行,此時recv等方法返回的就是S4-S1,即從內核拷貝的字節數。
圖1描述的場景是最簡單的1種場景,下面咱們來看看上述步驟是怎樣經過內核代碼實現的(如下代碼爲2.6.18內核代碼)。
咱們知道,linux對中斷的處理是分爲上半部和下半部的,這是處於系統總體效率的考慮。咱們將要介紹的都是在網絡軟中斷的下半部裏,例如這個tcp_v4_rcv方法。圖1中的第1-4步都是在這個方法裏完成的。
- int tcp_v4_rcv(struct sk_buff *skb)
- {
- ... ...
-
-
- if (!sock_owned_by_user(sk)) {
- {
-
- if (!tcp_prequeue(sk, skb))
- ret = tcp_v4_do_rcv(sk, skb);
- }
- } else
- sk_add_backlog(sk, skb);
- ... ...
- }
圖1第1步裏,咱們從網絡上收到了序號爲S1-S2的包。此時,沒有用戶進程在讀取套接字,所以,sock_owned_by_user(sk)會返回0。因此,tcp_prequeue方法將獲得執行。簡單看看它:
- static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
- {
- struct tcp_sock *tp = tcp_sk(sk);
-
-
- if (!sysctl_tcp_low_latency && tp->ucopy.task) {
-
- __skb_queue_tail(&tp->ucopy.prequeue, skb);
- tp->ucopy.memory += skb->truesize;
-
- if (tp->ucopy.memory > sk->sk_rcvbuf) {
- while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
-
- sk->sk_backlog_rcv(sk, skb1);
- }
- } else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
-
- wake_up_interruptible(sk->sk_sleep);
- }
-
- return 1;
- }
-
- return 0;
- }
因爲tp->ucopy.task此時是NULL,因此咱們收到的第1個報文在tcp_prequeue函數裏直接返回了0,所以,將由 tcp_v4_do_rcv方法處理。
- int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
- {
- if (sk->sk_state == TCP_ESTABLISHED) {
-
- if (tcp_rcv_established(sk, skb, skb->h.th, skb->len))
- goto reset;
-
- return 0;
- }
- ... ...
- }
tcp_rcv_established方法在圖1裏,主要調用tcp_data_queue方法將報文放入隊列中,繼續看看它又幹了些什麼事:
- static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
- {
- struct tcp_sock *tp = tcp_sk(sk);
-
-
- if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
-
- if (tcp_receive_window(tp) == 0)
- goto out_of_window;
-
-
- if (tp->ucopy.task == current &&
- tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
- sock_owned_by_user(sk) && !tp->urg_data) {
-
- if (!skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)) {
- tp->ucopy.len -= chunk;
- tp->copied_seq += chunk;
- }
- }
-
- if (eaten <= 0) {
- queue_and_out:
-
- __skb_queue_tail(&sk->sk_receive_queue, skb);
- }
-
- tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
-
-
- if (!skb_queue_empty(&tp->out_of_order_queue)) {
-
- tcp_ofo_queue(sk);
- }
- }
- ... ...
-
-
- if (!skb_peek(&tp->out_of_order_queue)) {
- __skb_queue_head(&tp->out_of_order_queue,skb);
- } else {
- ... ...
- __skb_append(skb1, skb, &tp->out_of_order_queue);
- }
- }
圖1第4步時,正是經過tcp_ofo_queue方法把以前亂序的S3-S4報文插入receive隊列的。
- static void tcp_ofo_queue(struct sock *sk)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- __u32 dsack_high = tp->rcv_nxt;
- struct sk_buff *skb;
-
- while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
- ... ...
-
- __skb_unlink(skb, &tp->out_of_order_queue);
-
- __skb_queue_tail(&sk->sk_receive_queue, skb);
-
- tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
- }
- }
下面再介紹圖1第6步提到的tcp_recvmsg方法。
-
- int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
- size_t len, int nonblock, int flags, int *addr_len)
- {
-
- lock_sock(sk);
-
-
- err = -ENOTCONN;
-
-
- timeo = sock_rcvtimeo(sk, nonblock);
-
-
-
- seq = &tp->copied_seq;
-
- if (flags & MSG_PEEK) {
-
- peek_seq = tp->copied_seq;
- seq = &peek_seq;
- }
-
-
-
- target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
-
-
- do {
-
- skb = skb_peek(&sk->sk_receive_queue);
- do {
-
- if (!skb)
- break;
-
-
-
- offset = *seq - TCP_SKB_CB(skb)->seq;
-
- if (skb->h.th->syn)
- offset--;
-
- if (offset < skb->len)
- goto found_ok_skb;
-
- skb = skb->next;
- } while (skb != (struct sk_buff *)&sk->sk_receive_queue);
-
-
- if (copied >= target && !sk->sk_backlog.tail)
- break;
-
-
- if (copied) {
- ... ...
- } else {
-
- if (sk->sk_shutdown & RCV_SHUTDOWN)
- break;
-
-
- if (!timeo) {
-
- copied = -EAGAIN;
- break;
- }
- ... ...
- }
-
-
- if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
-
- if (!skb_queue_empty(&tp->ucopy.prequeue))
- goto do_prequeue;
- }
-
-
- if (copied >= target) {
-
- release_sock(sk);
- lock_sock(sk);
- } else
- sk_wait_data(sk, &timeo);
-
- if (user_recv) {
- if (tp->rcv_nxt == tp->copied_seq &&
- !skb_queue_empty(&tp->ucopy.prequeue)) {
- do_prequeue:
-
- tcp_prequeue_process(sk);
- }
- }
-
-
- continue;
-
- found_ok_skb:
-
-
- used = skb->len - offset;
-
- if (len < used)
- used = len;
-
-
- if (!(flags & MSG_TRUNC)) {
- {
-
- err = skb_copy_datagram_iovec(skb, offset,
- msg->msg_iov, used);
- }
- }
-
-
- *seq += used;
-
- copied += used;
-
- len -= used;
-
- ... ...
- } while (len > 0);
-
-
- if (user_recv) {
-
- if (!skb_queue_empty(&tp->ucopy.prequeue)) {
- tcp_prequeue_process(sk);
- }
-
-
- tp->ucopy.task = NULL;
- tp->ucopy.len = 0;
- }
-
-
- release_sock(sk);
-
- return copied;
- }
圖2給出了第2種場景,這裏涉及到prequeue隊列。用戶進程調用recv方法時,鏈接上沒有任何接收並緩存到內核的報文,而socket是阻塞的,因此進程睡眠了。而後網卡中收到了TCP鏈接上的報文,此時prequeue隊列開始產生做用。圖2中tcp_low_latency爲默認的0,套接字socket的SO_RCVLOWAT是默認的1,仍然是阻塞socket,以下圖:
簡單描述上述11個步驟:
一、用戶進程分配了一塊len大小的內存,將其傳入recv這樣的函數,同時socket參數皆爲默認,即阻塞的、SO_RCVLOWAT爲1。調用接收方法,其中flags參數爲0。
二、C庫和內核最終調用到tcp_recvmsg方法來處理。
三、鎖住socket。
四、因爲此時receive、prequeue、backlog隊列都是空的,即沒有拷貝1個字節的消息到用戶內存中,而咱們的最低要求是拷貝至少SO_RCVLOWAT爲1長度的消息。此時,開始進入阻塞式套接字的等待流程。最長等待時間爲SO_RCVTIMEO指定的時間。
這個等待函數叫作sk_wait_data,有必要看下其實現:
- int sk_wait_data(struct sock *sk, long *timeo)
- {
-
- rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
- }
sk_wait_event也值得咱們簡單看下:
- #define sk_wait_event(__sk, __timeo, __condition) \
- ({ int rc; \
- release_sock(__sk); \
- rc = __condition; \
- if (!rc) { \
- *(__timeo) = schedule_timeout(*(__timeo)); \
- } \
- lock_sock(__sk); \
- rc = __condition; \
- rc; \
- })
注意,它在睡眠前會調用release_sock,這個方法會釋放socket鎖,使得下面的第5步中,新到的報文再也不只能進入backlog隊列。
五、這個套接字上指望接收的序號也是S1,此時網卡剛好收到了S1-S2的報文,在tcp_v4_rcv方法中,經過調用tcp_prequeue方法把報文插入到prequeue隊列中。
六、插入prequeue隊列後,此時會接着調用wake_up_interruptible方法,喚醒在socket上睡眠的進程。參見tcp_prequque方法。
七、用戶進程被喚醒後,從新調用lock_sock接管了這個socket,此後再進來的報文都只能進入backlog隊列了。
八、進程醒來後,先去檢查receive隊列,固然仍然是空的;再去檢查prequeue隊列,發現有一個報文S1-S2,正好是socket鏈接待拷貝的起始序號S1,因而,從prequeue隊列中取出這個報文並把內容複製到用戶內存中,再釋放內核中的這個報文。
九、目前已經拷貝了S2-S1個字節到用戶態,檢查這個長度是否超過了最低閥值(即len和SO_RCVLOWAT的最小值)。
十、因爲SO_RCVLOWAT使用了默認的1,因此準備返回用戶。此時會順帶再看看backlog隊列中有沒有數據,如有,則檢查這個無序的隊列中是否有能夠直接拷貝給用戶的報文。固然,此時是沒有的。因此準備返回,釋放socket鎖。
十一、返回用戶已經拷貝的字節數。
圖3給出了第3種場景。這個場景中,咱們把系統參數tcp_low_latency設爲1,socket上設置了SO_RCVLOWAT屬性的值。服務器先是收到了S1-S2這個報文,但S2-S1的長度是小於SO_RCVLOWAT的,用戶進程調用recv方法讀套接字時,雖然讀到了一些,但沒有達到最小閥值,因此進程睡眠了,與此同時,在睡眠前收到的亂序的S3-S4包直接進入backlog隊列。此時先到達了S2-S3包,因爲沒有使用prequeue隊列,而它起始序號正是下一個待拷貝的值,因此直接拷貝到用戶內存中,總共拷貝字節數已知足SO_RCVLOWAT的要求!最後在返回用戶前把backlog隊列中S3-S4報文也拷貝給用戶了。以下圖:
簡明描述上述15個步驟:
一、內核收到報文S1-S2,S1正是這個socket鏈接上待接收的序號,所以,直接將它插入有序的receive隊列中。
二、用戶進程所處的linux操做系統上,將sysctl中的tcp_low_latency設置爲1。這意味着,這臺服務器但願TCP進程可以更及時的接收到TCP消息。用戶調用了recv方法接收socket上的消息,這個socket上設置了SO_RCVLOWAT屬性爲某個值n,這個n是大於S2-S1,也就是第1步收到的報文大小。這裏,仍然是阻塞socket,用戶依然是分配了足夠大的len長度內存以接收TCP消息。
三、經過tcp_recvmsg方法來完成接收工做。先鎖住socket,避免併發進程讀取同一socket的同時,也在告訴內核網絡軟中斷處理到這一socket時要有不一樣行爲,如第6步。
四、準備處理內核各個接收隊列中的報文。
五、receive隊列中的有序報文可直接拷貝,在檢查到S2-S1是小於len以後,將報文內容拷貝到用戶態內存中。
六、在第5步進行的同時,socket是被鎖住的,這時內核又收到了一個S3-S4報文,所以報文直接進入backlog隊列。注意,這個報文不是有序的,由於此時鏈接上期待接收序號爲S2。
七、在第5步,拷貝了S2-S1個字節到用戶內存,它是小於SO_RCVLOWAT的,所以,因爲socket是阻塞型套接字(超時時間在本文中忽略),進程將不得不轉入睡眠。轉入睡眠以前,還會幹一件事,就是處理backlog隊列裏的報文,圖2的第4步介紹過休眠方法sk_wait_data,它在睡眠前會執行release_sock方法,看看是如何實現的:
- void fastcall release_sock(struct sock *sk)
- {
- mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
-
- spin_lock_bh(&sk->sk_lock.slock);
-
- if (sk->sk_backlog.tail)
- __release_sock(sk);
-
- sk->sk_lock.owner = NULL;
- if (waitqueue_active(&sk->sk_lock.wq))
- wake_up(&sk->sk_lock.wq);
- spin_unlock_bh(&sk->sk_lock.slock);
- }
再看看__release_sock方法是如何遍歷backlog隊列的:
- static void __release_sock(struct sock *sk)
- {
- struct sk_buff *skb = sk->sk_backlog.head;
-
-
- do {
- sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
- bh_unlock_sock(sk);
-
- do {
- struct sk_buff *next = skb->next;
-
- skb->next = NULL;
-
- sk->sk_backlog_rcv(sk, skb);
-
- cond_resched_softirq();
-
- skb = next;
- } while (skb != NULL);
-
- bh_lock_sock(sk);
- } while((skb = sk->sk_backlog.head) != NULL);
- }
此時遍歷到S3-S4報文,但由於它是失序的,因此從backlog隊列中移入out_of_order隊列中(參見上文說過的tcp_ofo_queue方法)。
八、進程休眠,直到超時或者receive隊列不爲空。
九、內核接收到了S2-S3報文。注意,這裏因爲打開了tcp_low_latency標誌位,這個報文是不會進入prequeue隊列以待進程上下文處理的。
十、此時,因爲S2是鏈接上正要接收的序號,同時,有一個用戶進程正在休眠等待接收數據中,且它要等待的數據起始序號正是S2,因而,這種種條件下,使得這一步同時也是網絡軟中斷執行上下文中,把S2-S3報文直接拷貝進用戶內存。
十一、上文介紹tcp_data_queue方法時你們能夠看到,每處理完1個有序報文(不管是拷貝到receive隊列仍是直接複製到用戶內存)後都會檢查out_of_order隊列,看看是否有報文能夠處理。那麼,S3-S4報文剛好是待處理的,因而拷貝進用戶內存。而後喚醒用戶進程。
十二、用戶進程被喚醒了,固然喚醒後會先來拿到socket鎖。如下執行又在進程上下文中了。
1三、此時會檢查已拷貝的字節數是否大於SO_RCVLOWAT,以及backlog隊列是否爲空。二者皆知足,準備返回。
1四、釋放socket鎖,退出tcp_recvmsg方法。
1五、返回用戶已經複製的字節數S4-S1。
好了,這3個場景讀完,想必你們對於TCP的接收流程是怎樣的已經很是清楚了,本文起始的6個問題也在這一大篇中都涉及到了。下一篇咱們來討論TCP鏈接的關閉。
TCP鏈接的關閉有兩個方法close和shutdown,這篇文章將盡可能精簡的說明它們分別作了些什麼。
爲方便閱讀,咱們能夠帶着如下5個問題來閱讀本文:
一、當socket被多進程或者多線程共享時,關閉鏈接時有何區別?
二、關鏈接時,若鏈接上有來自對端的還未處理的消息,會怎麼處理?
三、關鏈接時,若鏈接上有本進程待發送卻將來得及發送出的消息,又會怎麼處理?
四、so_linger這個功能的用處在哪?
五、對於監聽socket執行關閉,和對處於ESTABLISH這種通信的socket執行關閉,有何區別?
下面分三部分進行:首先說說多線程多進程關閉鏈接的區別;再用一幅流程圖談談close;最後用一幅流程圖說說shutdown。
先不提其原理和實現,從多進程、多線程下 close和shutdown方法調用時的區別提及。
看看close與shutdown這兩個系統調用對應的內核函數:(參見unistd.h文件)
- #define __NR_close 3
- __SYSCALL(__NR_close, sys_close)
- #define __NR_shutdown 48
- __SYSCALL(__NR_shutdown, sys_shutdown)
但sys_close和sys_shutdown這兩個系統調用最終是由tcp_close和tcp_shutdown方法來實現的,調用過程以下圖所示:
sys_shutdown與多線程和多進程都沒有任何關係,而sys_close則否則,上圖中能夠看到,層層封裝調用中有一個方法叫fput,它有一個引用計數,記錄這個socket被引用了多少次。在說明多線程或者多進程調用close的區別前,先在代碼上簡單看下close是怎麼調用的,對內核代碼沒興趣的同窗能夠僅看fput方法:
- void fastcall fput(struct file *file)
- {
- if (atomic_dec_and_test(&file->f_count))
- __fput(file);
- }
當這個socket的引用計數f_count不爲0時,是不會觸發到真正關閉TCP鏈接的tcp_close方法的。
那麼,這個引用計數的意義何在呢?爲了說明它,先要說道下進程與線程的區別。
你們知道,所謂線程其實就是「輕量級」的進程。建立進程只能是一個進程(父進程)建立另外一個進程(子進程),子進程會複製父進程的資源,這裏的」複製「針對不一樣的資源其意義是不一樣的,例如對內存、文件、TCP鏈接等。建立進程是由clone系統調用實現的,而建立線程時一樣也是clone實現的,只不過clone的參數不一樣,其行爲也很不一樣。這個話題是很大的,這裏咱們僅討論下TCP鏈接。
在clone系統調用中,會調用方法copy_files來拷貝文件描述符(包括socket)。建立線程時,傳入的flag參數中包含標誌位CLONE_FILES,此時,線程將會共享父進程中的文件描述符。而建立進程時沒有這個標誌位,這時,會把進程打開的全部文件描述符的引用計數加1,即把file數據結構的f_count成員加1,以下:
- static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
- {
- if (clone_flags & CLONE_FILES) {
- goto out;
- }
- newf = dup_fd(oldf, &error);
- out:
- return error;
- }
再看看dup_fd方法:
- static struct files_struct *dup_fd(struct files_struct *oldf, int *errorp)
- {
- for (i = open_files; i != 0; i--) {
- struct file *f = *old_fds++;
- if (f) {
- get_file(f);
- }
- }
- }
get_file宏就會加引用計數。
- #define get_file(x) atomic_inc(&(x)->f_count)
因此,子進程會將父進程中已經創建的socket加上引用計數。當進程中close一個socket時,只會減小引用計數,僅當引用計數爲0時纔會觸發tcp_close。
到這裏,對於第一個問題的close調用天然有告終論:單線程(進程)中使用close與多線程中是一致的,但這二者與多進程的行爲並不一致,多進程中共享的同一個socket必須都調用了close纔會真正的關閉鏈接。
而shutdown則否則,這裏是沒有引用計數什麼事的,只要調用了就會去試圖按需關閉鏈接。因此,調用shutdown與多線程、多進程無關。
下面咱們首先深刻探討下close的行爲,由於close比較shutdown來講要複雜許多。順便回答其他四個問題。
TCP鏈接是一種雙工的鏈接,何謂雙工?即鏈接雙方能夠並行的發送或者接收消息,而無須顧及對方此時到底在發仍是收消息。這樣,關閉鏈接時,就存在3種情形:徹底關閉鏈接;關閉發送消息的功能;關閉接收消息的功能。其中,後二者就叫作半關閉,由shutdown實現(因此 shutdown多出一個參數正是控制關閉發送或者關閉接收),前者由close實現。
TCP鏈接是一種可靠的鏈接,在這裏能夠這麼理解:既要確認本機發出的包獲得確認,又要確認收到的任何消息都已告知鏈接的對端。
如下主要從雙工、可靠性這兩點上理解鏈接的關閉。
TCP雙工的這個特性使得鏈接的正常關閉須要四次握手,其含義爲:主動端關閉了發送的功能;被動端承認;被動端也關閉了發送的功能;主動端承認。
但還存在程序異常的情形,此時,則經過異常的那端發送RST復位報文通知另外一端關閉鏈接。
下圖是close的主要流程:
這個圖稍複雜,這是由於它覆蓋了關閉監聽句柄、關閉普通鏈接、關閉設置了SO_LINGER的鏈接這三種主要場景。
1)關閉監聽句柄
先從最右邊的分支說說關閉監聽socket的那些事。用於listen的監聽句柄也是使用close關閉,關閉這樣的句柄含義固然很不一樣,它自己並不對應着某個TCP鏈接,可是,附着在它之上的卻可能有半成品鏈接。什麼意思呢?以前說過TCP是雙工的,它的打開須要三次握手,三次握手也就是3個步驟,其含義爲:客戶端打開接收、發送的功能;服務器端承認並也打開接收、發送的功能;客戶端承認。當第一、2步驟完成、第3步步驟未完成時,就會在服務器上有許多半鏈接,close這個操做主要是清理這些鏈接。
參照上圖,close首先會移除keepalive定時器。keepalive功能經常使用於服務器上,防止僵死、異常退出的客戶端佔用服務器鏈接資源。移除此定時器後,若ESTABLISH狀態的TCP鏈接在tcp_keepalive_time時間(如服務器上常配置爲2小時)內沒有通信,服務器就會主動關閉鏈接。
接下來,關閉每個半鏈接。如何關閉半鏈接?這時固然不能發FIN包,即正常的四次握手關閉鏈接,而是會發送RST復位標誌去關閉請求。處理完全部半打開的鏈接close的任務就基本完成了。
2)關閉普通ESTABLISH狀態的鏈接(未設置so_linger)
首先檢查是否有接收到卻未處理的消息。
若是close調用時存在收到遠端的、沒有處理的消息,這時根據close這一行爲的意義,是要丟棄這些消息的。但丟棄消息後,意味着鏈接遠端誤覺得發出的消息已經被本機收處處理了(由於ACK包確認過了),但實際上確是收到未處理,此時也不能使用正常的四次握手關閉,而是會向遠端發送一個RST非正常復位關閉鏈接。這個作法的依據請參考draft-ietf-tcpimpl-prob-03.txt文檔3.10節,
Failure to RST on close with data pending。因此,這也要求咱們程序員在關閉鏈接時,要確保已經接收、處理了鏈接上的消息。
若是此時沒有未處理的消息,那麼進入發送FIN來關閉鏈接的階段。
這時,先看看是否有待發送的消息。前一篇已經說過,發消息時要計算滑動窗口、擁塞窗口、angle算法等,這些因素可能致使消息會延遲發送的。若是有待發送的消息,那麼要盡力保證這些消息都發出去的。因此,會在最後一個報文中加入FIN標誌,同時,關閉用於減小網絡中小報文的angle算法,向鏈接對端發送消息。若是沒有待發送的消息,則構造一個報文,僅含有FIN標誌位,發送出去關閉鏈接。
3)使用了so_linger的鏈接
首先要澄清,爲什麼要有so_linger這個功能?由於咱們可能有強可靠性的需求,也就是說,必須確保發出的消息、FIN都被對方收到。例如,有些響應發出後調用close關閉鏈接,接下來就會關閉進程。若是close時發出的消息其實丟失在網絡中了,那麼,進程忽然退出時鏈接上發出的RST就可能被對方收到,並且,以前丟失的消息不會有重發來保障可靠性了。
so_linger用來保證對方收到了close時發出的消息,即,至少須要對方經過發送ACK且到達本機。
怎麼保證呢?等待!close會阻塞住進程,直到確認對方收到了消息再返回。然而,網絡環境又得複雜的,若是對方老是不響應怎麼辦?因此還須要l_linger這個超時時間,控制close阻塞進程的最長時間。注意,務必慎用so_linger,它會在不經意間下降你程序中代碼的執行速度(close的阻塞)。
因此,當這個進程設置了so_linger後,前半段依然沒變化。檢查是否有未讀消息,如有則發RST關鏈接,不會觸發等待。接下來檢查是否有未發送的消息時與第2種情形一致,設好FIN後關閉angle算法發出。接下來,則會設置最大等待時間l_linger,而後開始將進程睡眠,直到確認對方收到後纔會醒來,將控制權交還給用戶進程。
這裏須要注意,so_linger不是確保鏈接被四次握手關閉再使close返回,而只是保證我方發出的消息都已被對方收到。例如,若對方程序寫的有問題,當它收到FIN進入CLOSE_WAIT狀態,卻一直不調用close發出FIN,此時,對方仍然會經過ACK確認,我方收到了ACK進入FIN_WAIT2狀態,但沒收到對方的FIN,我方的close調用卻不會再阻塞,close直接返回,控制權交還用戶進程。
從上圖可知,so_linger還有個偏門的用法,若l_linger超時時間竟被設爲0,則不會觸發FIN包的發送,而是直接RST復位關閉鏈接。我我的認爲,這種玩法確沒多大用處。
最後作個總結。調用close時,可能致使發送RST復位關閉鏈接,例若有未讀消息、打開so_linger但l_linger卻爲0、關閉監聽句柄時半打開的鏈接。更多時會致使發FIN來四次握手關閉鏈接,但打開so_linger可能致使close阻塞住等待着對方的ACK代表收到了消息。
最後來看看較爲簡單的shutdown。
1)shutdown可攜帶一個參數,取值有3個,分別意味着:只關閉讀、只關閉寫、同時關閉讀寫。
對於監聽句柄,若是參數爲關閉寫,顯然沒有任何意義。但關閉讀從某方面來講是有意義的,例如再也不接受新的鏈接。看看最右邊藍色分支,針對監聽句柄,若參數爲關閉寫,則不作任何事;若爲關閉讀,則把端口上的半打開鏈接使用RST關閉,與close一模一樣。
2)若shutdown的是半打開的鏈接,則發出RST來關閉鏈接。
3)若shutdown的是正常鏈接,那麼關閉讀其實與對端是沒有關係的。只要本機把接收掉的消息丟掉,其實就等價於關閉讀了,並不必定非要對端關閉寫的。實際上,shutdown正是這麼幹的。若參數中的標誌位含有關閉讀,只是標識下,當咱們調用read等方法時這個標識就起做用了,會使進程讀不到任何數據。
4)若參數中有標誌位爲關閉寫,那麼下面作的事與close是一致的:發出FIN包,告訴對方,本機不會再發消息了。
以上,就是close與shutdown的主要行爲,同時也回答了本文最初的5個問題。下一篇,咱們開始討論多路複用中常見的epoll。