筆者一直覺得如果能知道從應用到框架再到操作系統的每一處代碼,是一件Exciting的事情。上篇博客講了socket的阻塞和非阻塞,這篇就開始談一談socket的close(以tcp為例且基于linux-2.6.24內核版本)
TCP關閉狀態轉移圖
眾所周知,TCP的close過程是四次揮手,狀態機的變遷也逃不出TCP狀態轉移圖,如下圖所示:
tcp的關閉主要分主動關閉、被動關閉以及同時關閉(特殊情況,不做描述)
主動關閉
close(fd)的過程
以C語言為例,在我們關閉socket的時候,會使用close(fd)函數:
intsocket_fd;
socket_fd = socket(AF_INET,SOCK_STREAM,0);
...
// 此處通過文件描述符關閉對應的socket
close(socket_fd)
而close(int fd)又是通過系統調用sys_close來執行的:
asmlinkage longsys_close(unsignedintfd)
{
// 清除(close_on_exec即退出進程時)的位圖標記
FD_CLR(fd,fdt->close_on_exec);
// 釋放文件描述符
// 將fdt->open_fds即打開的fd位圖中對應的位清除
// 再將fd掛入下一個可使用的fd以便復用
__put_unused_fd(files,fd);
// 調用file_pointer的close方法真正清除
retval = filp_close(filp,files);
}
我們看到最終是調用的filp_close方法:
緊接著我們進入fput:
同一個file(socket)有多個引用的情況很常見,例如下面的例子:
所以在多進程的socket服務器編寫過程中,父進程也需要close(fd)一次,以免socket無法最終關閉
然后就是_fput函數了:
由于我們討論的是socket的close,所以,我們現在探查下file->f_op->release在socket情況下的實現:
f_op->release的賦值
我們跟蹤創建socket的代碼,即
socket_file_ops的實現為:
staticconststructfile_operations socket_file_ops = {
.owner = THIS_MODULE,
......
// 我們在這里只考慮sock_close
.release = sock_close,
......
};
繼續跟蹤:
在上一篇博客中,我們知道sock->ops為下圖所示:
即(在這里我們僅考慮tcp,即sk_prot=tcp_prot):
關于fd與socket的關系如下圖所示:
上圖中紅色線標注的是close(fd)的調用鏈
tcp_close
四次揮手
現在就是我們的四次揮手環節了,其中上半段的兩次揮手下圖所示:
首先,在tcp_close_state(sk)中已經將狀態設置為fin_wait1,并調用tcp_send_fin
voidtcp_send_fin(structsock *sk)
{
......
// 這邊設置flags為ack和fin
TCP_SKB_CB(skb)->flags = (TCPCB_FLAG_ACK | TCPCB_FLAG_FIN);
......
// 發送fin包,同時關閉nagle
__tcp_push_pending_frames(sk,mss_now,TCP_NAGLE_OFF);
}
如上圖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的話,則可以按照下列配置:
net.ipv4.tcp_fin_timeout
/sbin/sysctl -wnet.ipv4.tcp_fin_timeout=30
如下圖所示:
有這樣一步的原因是防止對端由于種種原因始終沒有發送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函數
voidtcp_time_wait(structsock *sk,intstate,inttimeo)
{
// 建立inet_timewait_sock
tw = inet_twsk_alloc(sk,state);
// 放到bucket的具體位置等待定時器刪除
inet_twsk_schedule(tw, &tcp_death_row,time,TCP_TIMEWAIT_LEN);
// 設置sk狀態為TCP_CLOSE,然后回收sk資源
tcp_done(sk);
}
具體的定時器操作函數為inet_twdr_twcal_tick,這邊就不做描述了
被動關閉
close_wait
在tcp的socket時候,如果是established狀態,接收到了對端的FIN,則是被動關閉狀態,會進入close_wait狀態,如下圖Step1所示:
具體代碼如下所示:
我們再看下tcp_fin
這邊有意思的點是,收到對端的fin之后并不會立即發送ack告知對端收到了,而是等有數據攜帶一塊發送,或者等攜帶重傳定時器到期后發送ack。
如果對端關閉了,應用端在read的時候得到的返回值是0,此時就應該手動調用close去關閉連接
if(recv(sockfd,buf,MAXLINE,0) == 0){
close(sockfd)
}
我們看下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的時間
/etc/sysctl.conf
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
默認值如上面所示,設置的很大,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網絡協議棧代碼的人。
-
Linux
+關注
關注
87文章
11322瀏覽量
209857 -
Socket
+關注
關注
0文章
212瀏覽量
34743
原文標題:從 Linux 源碼看 socket 的 close
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論