從 Linux 原始碼看 socket 的 close

發表於2018-08-16

從linux原始碼看socket的close

筆者一直覺得如果能知道從應用到框架再到作業系統的每一處程式碼,是一件Exciting的事情。上篇部落格講了socket的阻塞和非阻塞,這篇就開始談一談socket的close(以tcp為例且基於linux-2.6.24核心版本)

TCP關閉狀態轉移圖:

眾所周知,TCP的close過程是四次揮手,狀態機的變遷也逃不出TCP狀態轉移圖,如下圖所示: 輸入圖片說明
tcp的關閉主要分主動關閉、被動關閉以及同時關閉(特殊情況,不做描述)

主動關閉

close(fd)的過程

以C語言為例,在我們關閉socket的時候,會使用close(fd)函式:

而close(int fd)又是通過系統呼叫sys_close來執行的:

我們看到最終是呼叫的filp_close方法:

緊接著我們進入fput:

同一個file(socket)有多個引用的情況很常見,例如下面的例子:
輸入圖片說明
所以在多程式的socket伺服器編寫過程中,父程式也需要close(fd)一次,以免socket無法最終關閉

然後就是_fput函式了:

由於我們討論的是socket的close,所以,我們現在探查下file->f_op->release在socket情況下的實現:

f_op->release的賦值

我們跟蹤建立socket的程式碼,即

socket_file_ops的實現為:

繼續跟蹤:

在上一篇部落格中,我們知道sock->ops為下圖所示: 輸入圖片說明
即(在這裡我們僅考慮tcp,即sk_prot=tcp_prot):

關於fd與socket的關係如下圖所示:
輸入圖片說明
上圖中紅色線標註的是close(fd)的呼叫鏈

tcp_close

四次揮手

現在就是我們的四次揮手環節了,其中上半段的兩次揮手下圖所示:
輸入圖片說明
首先,在tcp_close_state(sk)中已經將狀態設定為fin_wait1,並呼叫tcp_send_fin

如上圖Step1所示。 接著,主動關閉的這一端等待對端的ACK,如果ACK回來了,就設定TCP狀態為FIN_WAIT2,如上圖Step2所示,具體程式碼如下:

值的注意的是,從TCP_FIN_WAIT1變遷到TCP_FIN_WAIT2之後,還呼叫tcp_time_wait設定一個TCP_FIN_WAIT2定時器,在tmo+(2MSL或者基於RTO計算超時)超時後會直接變遷到closed狀態(不過此時已經是inet_timewait_sock了)。這個超時時間可以配置,如果是ipv4的話,則可以按照下列配置:

如下圖所示: 輸入圖片說明
有這樣一步的原因是防止對端由於種種原因始終沒有傳送fin,防止一直處於FIN_WAIT2狀態。

接著在FIN_WAIT2狀態等待對端的FIN,完成後面兩次揮手: 輸入圖片說明
由Step1和Step2將狀態置為了FIN_WAIT_2,然後接收到對端傳送的FIN之後,將會將狀態設定為time_wait,如下程式碼所示:

time_wait狀態時,原socket會被destroy,然後新建立一個inet_timewait_sock,這樣就能及時的將原socket使用的資源回收。而inet_timewait_sock被掛入一個bucket中,由 inet_twdr_twcal_tick定時從bucket中將超過(2MSL或者基於RTO計算的時間)的time_wait的例項刪除。 我們來看下tcp_time_wait函式

具體的定時器操作函式為inet_twdr_twcal_tick,這邊就不做描述了

被動關閉

close_wait

在tcp的socket時候,如果是established狀態,接收到了對端的FIN,則是被動關閉狀態,會進入close_wait狀態,如下圖Step1所示:
輸入圖片說明
具體程式碼如下所示:

我們再看下tcp_fin

這邊有意思的點是,收到對端的fin之後並不會立即傳送ack告知對端收到了,而是等有資料攜帶一塊傳送,或者等攜帶重傳定時器到期後傳送ack。

如果對端關閉了,應用端在read的時候得到的返回值是0,此時就應該手動呼叫close去關閉連線

我們看下recv是怎麼處理fin包,從而返回0的,上一篇部落格可知,recv最後呼叫tcp_rcvmsg,由於比較複雜,我們分兩段來看:
tcp_recvmsg第一段

上面程式碼的處理過程如下圖所示: 輸入圖片說明
我們看下tcp_recmsg的第二段:

由上面程式碼可知,一旦當前skb讀完了而且攜帶有fin標識,則不管有沒有讀到使用者期望的位元組數量都會返回已讀到的位元組數。下一次再讀取的時候則在剛才描述的tcp_rcvmsg上半段直接不讀取任何資料再跳轉到found_fin_ok並返回0。這樣應用就能感知到對端已經關閉了。 如下圖所示:
輸入圖片說明

last_ack

應用層在發現對端關閉之後已經是close_wait狀態,這時候再呼叫close的話,會將狀態改為last_ack狀態,併傳送本端的fin,如下程式碼所示:

在接收到主動關閉端的last_ack之後,則呼叫tcp_done(sk)設定sk為tcp_closed狀態,並回收sk的資源,如下程式碼所示:

上述程式碼就是被動關閉端的後兩次揮手了,如下圖所示:

輸入圖片說明

出現大量close_wait的情況

linux中出現大量close_wait的情況一般是應用在檢測到對端fin時沒有及時close當前連線。有一種可能如下圖所示: 輸入圖片說明
當出現這種情況,通常是minIdle之類引數的配置不對(如果連線池有定時收縮連線功能的話)。給連線池加上心跳也可以解決這種問題。
如果應用close的時間過晚,對端已經將連線給銷燬。則應用傳送給fin給對端,對端會由於找不到對應的連線而傳送一個RST(Reset)報文。

作業系統何時回收close_wait

如果應用遲遲沒有呼叫close_wait,那麼作業系統有沒有一個回收機制呢,答案是有的。 tcp本身有一個包活(keep alive)定時器,在(keep alive)定時器超時之後,會強行將此連線關閉。可以設定tcp keep alive的時間

預設值如上面所示,設定的很大,7200s後超時,如果想快速回收close_wait可以設定小一點。但最終解決方案還是得從應用程式著手。
關於tcp keepalive包活定時器可見筆者另一篇部落格:
https://my.oschina.net/alchemystar/blog/833981

程式關閉時清理socket資源

程式在退出時候(無論kill,kill -9 或是正常退出)都會關閉當前程式中所有的fd(檔案描述符)

這樣我們又回到了部落格伊始的filp_close函式,對每一個是socket的fd傳送send_fin

Java GC時清理socket資源

Java的socket最終關聯到AbstractPlainSocketImpl,且其重寫了object的finalize方法

所以Java會在GC時刻會關閉沒有被引用的socket,但是切記不要寄希望於Java的GC,因為GC時刻並不是以未引用的socket數量來判斷的,所以有可能洩露了一堆socket,但仍舊沒有觸發GC。

總結

linux核心原始碼博大精深,閱讀其程式碼很費周折。之前讀《TCP/IP詳解卷二》的時候由於有先輩引導和梳理,所以看書中所使用的BSD原始碼並不覺得十分費勁。直到現在自己帶著問題獨立看linux原始碼的時候,儘管有之前的基礎,仍舊被其中的各種細節所迷惑。希望筆者這篇文章能幫助到閱讀linux網路協議棧程式碼的人。

相關文章