很多資料講了關于TCP的CLOSING和CLOSE_WAIT狀態(tài)以及所謂的優(yōu)雅關閉的細節(jié),多數(shù)側(cè)重與Linux的內(nèi)核實現(xiàn)(除了《UNIX網(wǎng)絡編程》)。本文不注重代碼細節(jié),只關注邏輯。所使用的工具,tcpdump,packetdrill以及ss。
關于ss可以先多說幾句,它展示的信息跟netstat差不多,只不過更加詳細。netstat的信息是通過procfs獲取的,本質(zhì)上來講就是遍歷/proc/net/netstat文件的內(nèi)容,然后將其組織成可讀的形式展示出來,然而ss則可以針對特定的五元組信息提供更加詳細的內(nèi)容,它不再通過procfs,而是用過Netlink來提取特定socket的信息,對于TCP而言,它可以提取到甚至tcp_info這種詳細的信息,它包括cwnd,ssthresh,rtt,rto等。
本文展示的邏輯使用了以下三樣工具:
1).packetdrill
使用packetdrill構(gòu)造出一系列的包序列,使得TCP進入CLOSING狀態(tài)或者CLOSE_WAIT狀態(tài)。
2).tcpdump/tshark
抓取packetdrill注入的數(shù)據(jù)包以及協(xié)議棧反饋的包,以確認數(shù)據(jù)包序列確實如TCP標準所述的那樣。
3).ss/netstat
通過ss抓取packetdrill相關套接字的tcp_info,再次確認細節(jié)。
我想,我使用上述的三件套解析了CLOSING狀態(tài)之后,接下來的CLOSE_WAIT狀態(tài)就可以當作練習了。
我來一個一個說。
1.關于CLOSING狀態(tài)
首先我來描述一下而不是細說概念。
什么是CLOSING狀態(tài)呢?我們來看一下下面的局部狀態(tài)圖:
也就是說,當兩端都主動發(fā)送FIN的時候,并且在收到對方對自己發(fā)送的FIN之前收到了對方發(fā)送的FIN的時候,兩邊就都進入了CLOSING狀態(tài),這個在狀態(tài)圖上顯示的很清楚。這個用俗話說就是”同時關閉“。時序圖我就不給出了,請自行搜索或者自己畫。
有很多人都說,這種狀態(tài)的TCP連接在系統(tǒng)中存在了好長時間并百思不得其解。這到底是為什么呢?通過狀態(tài)圖和時序圖,我們知道,在進入CLOSING狀態(tài)后,只要收到了對方對自己的FIN的ACK,就可以雙雙進入TIME_WAIT狀態(tài),因此,如果RTT處在一個可接受的范圍內(nèi),發(fā)出的FIN會很快被ACK從而進入到TIME_WAIT狀態(tài),CLOSING狀態(tài)應該持續(xù)的時間特別短。
以下是packetdrill腳本,很簡單的一個腳本:
0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0
0.100 < S 0:0(0) win 32792 < mss 1460,sackOK,nop,nop,nop,wscale 7 >
0.100 > S. 0:0(0) ack 1 win 5840 < mss 1460,nop,nop,sackOK,nop,wscale 7 >
0.200 < . 1:1(0) ack 1 win 257
0.200 accept(3, ..., ...) = 4
// 象征性寫入一些數(shù)據(jù),裝的像一點一個正常的TCP連接:握手-傳輸-揮手
0.250 write(4, ..., 1000) = 1000
0.300 < . 1:1(0) ack 1001 win 257
// 主動斷開,發(fā)送FIN
0.400 close(4) = 0
// 在未對上述close的FIN進行ACK前,先FIN
0.500 < F. 1:1(0) ack 1001 win 260
// 至此,成功進入同時關閉的CLOSING狀態(tài)。
// 由于packetdrill不能用dup調(diào)用,也好用多線程,為了維持進程不退出,只能等待
10000.000 close(4) = 0
同時,我啟用tcpdump抓包,確認了TCP狀態(tài)圖的細節(jié),即,還沒有收到對方對FIN的ACK時,收到了對方的FIN:
有個異常,沒有收到FIN的ACK(packetdrill沒有回復,這正常,因為腳本里本來就沒有這個語句),然而也沒有看到重傳,此時該連接應該是處于CLOSING狀態(tài)了,用ss來確認:
CLOSING 1 1 192.168.0.1:webcache 192.0.2.1:54442
cubic wscale:7,7 rto:2000 rtt:50/25 ssthresh:2 send 467.2Kbps rcv_space:5840
果然,進入了CLOSING狀態(tài)且沒有消失,時不我待,當過了2秒以后,ss的結(jié)果變成了:
CLOSING 1 1 192.168.0.1:webcache 192.0.2.1:54442
cubic wscale:7,7 rto:4000 rtt:50/25 ssthresh:2 send 467.2Kbps rcv_space:5840
明顯在退避!如果繼續(xù)觀察,你會發(fā)現(xiàn)rto退避到了64秒之多。在我的場景中,CLOSING狀態(tài)的套接字維持了兩分鐘之久。
然而,為什么呢?為什么CLOSING狀態(tài)會維持這么久?為什么它沒有繼續(xù)維持下去直到永久呢?
很明顯,一端的FIN發(fā)出去后,沒有收到ACK,因此會退避重發(fā),知道4次退避,即22222*2秒之久。現(xiàn)在的問題是,為什么重發(fā)FIN始終不成功呢?要是成功了的話,估計ACK瞬間也就回來了,那么CLOSING狀態(tài)也就可以進入TIME_WAIT了,但是沒有成功重傳FIN!
到此為止,我們知道,進入CLOSING狀態(tài)之后,兩邊都會等待接收自己FIN的ACK,一旦收到ACK,就會進入TIME_WAIT,如此反復,如果收不到ACK,則會不斷重傳FIN,直到忍無可忍,將socket銷毀。現(xiàn)在,我們集中于解釋為什么重傳沒有成功,但是請記住,并不是每次都這樣,只是在我這個packetdrill構(gòu)造的場景中會有重傳不成功,不然如果大概率不成功的話。豈不是每個CLOSING狀態(tài)都要維持很長時間??!!
在我的場景下,通過hook重傳函數(shù)以及抓包確認,發(fā)現(xiàn)所有的重傳雖然退避了,但是都沒有真正將數(shù)據(jù)包發(fā)送出去,究其原因,最終確認問題出在以下代碼上:
if (atomic_read(&sk- >sk_wmem_alloc) >
min(sk- >sk_wmem_queued + (sk- >sk_wmem_queued > > 2), sk- >sk_sndbuf))
return -EAGAIN;
在Linux協(xié)議棧的實現(xiàn)中,tcp_retransmit_skb由tcp_retransmit_timer調(diào)用,即便是這里出了些問題沒有重傳成功,也還是會退避的,退避超時到期后,繼續(xù)在這里出錯,直到”不可容忍“銷毀socket。
我們可以得知,不管如何CLOSING狀態(tài)的TCP連接即便沒有收到對自己FIN的ACK,也不會永久保持下去,保持多久取決于自己發(fā)送FIN時刻的RTT,然后RTT計算出的RTO按照最大的退避次數(shù)來退避,直到最終執(zhí)行了固定次數(shù)的退避后,算出來的那個比較大的超時時間到期,然后TCP socket就銷毀了。
因此,CLOSING狀態(tài)并不可怕,起碼,不管怎樣,它有一個可控的銷毀時限。
...
現(xiàn)在我來解釋重傳不成功的細節(jié)。
我們知道,根據(jù)上述的代碼段,sk_wmem_alloc要足夠大,大到它比sk_wmem_queued+sk_wmem_queued/4更大的時候,才會返回錯誤造成重傳不成功,然而我們的packetdrill腳本中構(gòu)造的TCP連接的生命周期中僅僅傳輸了1000個字節(jié)的數(shù)據(jù),并且這1000個字節(jié)的數(shù)據(jù)得到了ACK,然后就結(jié)束了連接。一個socket保有一個sk_wmem_alloc字段,在skb交給這個socket的時候,該字段會增加skb長度的大小(skb本身大小包括skb數(shù)據(jù)大小),然而當skb不再由該socket持有的時候,也就是其被更底層的邏輯接管之后,socket的sk_wmem_alloc字段自然會減去skb長度的大小,這一切的過程由以下的函數(shù)決定,即skb_set_owner_w和skb_orphan。我們來看一下這兩個函數(shù):
static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
{
skb_orphan(skb);
skb- >sk = sk;
// sock_wfree回調(diào)中會遞減sk_wmem_alloc相應的大小,其大小就是skb- >truesize
skb- >destructor = sock_wfree;
/*
* We used to take a refcount on sk, but following operation
* is enough to guarantee sk_free() wont free this sock until
* all in-flight packets are completed
*/
atomic_add(skb- >truesize, &sk- >sk_wmem_alloc);
}
static inline void skb_orphan(struct sk_buff *skb)
{
// 調(diào)用回調(diào)函數(shù),遞減sk_wmem_alloc
if (skb- >destructor)
skb- >destructor(skb);
skb- >destructor = NULL;
skb- >sk = NULL;
}
也就是說,只要skb_orphan在skb通向網(wǎng)卡的路徑上被正確調(diào)用,就會保證sk_wmem_alloc的值隨著skb進入socket的管轄時而增加,而被實際發(fā)出后而減少。但是根據(jù)我的場景,事實好像不是這樣,sk_wmem_alloc的值只要發(fā)送一個skb就會增加,絲毫沒有減少的跡象...這是為什么呢?
有的時候,當你對某個邏輯理解足夠深入后,一定要相信自己的判斷,內(nèi)核存在BUG!內(nèi)核并不完美。我使用的是2.6.32老內(nèi)核,這個內(nèi)核我已經(jīng)使用了6年多,這是我在這個內(nèi)核上發(fā)現(xiàn)的第4個BUG了。
請注意,我的這個場景中,我使用了packetdrill來構(gòu)造數(shù)據(jù)包,而packetdrill使用了tun網(wǎng)卡。為什么使用真實網(wǎng)卡甚至使用loopback網(wǎng)卡就不會有問題呢?這進一步引導我去調(diào)查tun的代碼,果不其然,在其hard_xmit回調(diào)中沒有調(diào)用skb_orphan!也就說說,但凡使用2.6.32內(nèi)核版本tun驅(qū)動的,都會遇到這個問題呢。在tun的xmit中加入skb_orphan之后,問題消失,抓包你會發(fā)現(xiàn)大量的FIN重傳包,這些重傳隨著退避而間隔加大(注意,用ss命令比對一下rto字段的值和tcpdump抓取的實際值):
(為了驗證這個,我修改了packetdrill腳本,中間增加了很多的數(shù)據(jù)傳輸,以便盡快重現(xiàn)sk_wmem_alloc在使用tun時不遞減的問題)于是,我聯(lián)系了前公司的同事,讓他們修改OpenVPN使用的tun驅(qū)動代碼,因為當時確實出現(xiàn)過關于TCP使用OpenVPN隧道的重傳問題,然而,得到的答復卻是,xmit函數(shù)中已經(jīng)有skb_orphan了...然后我看了下代碼,發(fā)現(xiàn),公司的代碼已經(jīng)不存在問題了,因為我在前年搞tun多隊列的時候,已經(jīng)移植了3.9.6的tun驅(qū)動,這個問題已經(jīng)被修復。
自己曾經(jīng)做的事情,已然不再憶起...
2.關于CLOSE_WAIT狀態(tài)
和CLOSING狀態(tài)不同,CLOSE_WAIT狀態(tài)可能會持續(xù)更久更久的時間,導致無用的socket無法釋放,這個時間可能與應用進程的生命周期一樣久!
我們先看一下CLOSE_WAIT的局部狀態(tài)圖。
然后我來構(gòu)造一個packetdrill腳本:
0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0
0.100 < S 0:0(0) win 32792 < mss 1460,sackOK,nop,nop,nop,wscale 7 >
0.100 > S. 0:0(0) ack 1 win 14600 < mss 1460,nop,nop,sackOK,nop,wscale 7 >
0.200 < . 1:1(0) ack 1 win 257
0.200 accept(3, ..., ...) = 4
// 什么也不發(fā)了,直接斷開
0.350 < F. 1:1(0) ack 1 win 260
// 協(xié)議棧會對這個FIN進行ACK,然則應用程序不關閉連接的話...
//0.450 close(4) = 0
// 該連接就會變成CLOSE_WAIT,并且只要其socket引用計數(shù)不為0,就一直僵死在那里
2000.000 close(4) = 0
同樣的,我來展示抓包結(jié)果:
最后,和描述CLOSING狀態(tài)不同的是,隔了N個小時之后,我來看ss -ip的結(jié)果:
CLOSE-WAIT 1 0 192.168.0.1:webcache 192.0.2.1:53753 users:(("ppp",2399,8))
cubic wscale:7,7 rto:300 rtt:100/50 ato:40 cwnd:10 send 1.2Mbps rcv_space:14600
這個CLOSE_WAIT還在!這是為什么呢?
很遺憾,上述的packetdrill腳本并不能直觀地展示這個現(xiàn)象,還得靠我說一說。
CLOSE_WAIT是一端在收到FIN之后,發(fā)送自己的FIN之前所處的狀態(tài),那么很顯然,如果一個進程/線程始終不發(fā)送FIN,那么在該連接所隸屬的socket的生命周期內(nèi),這個socket就會一直存在,我們知道,在UNIX/Linux/WinSock中,socket作為一個描述符出現(xiàn),只要進程/線程繼續(xù)持有它,它就會一直存在,因此大多數(shù)情況下進程/線程的生命周期內(nèi),此TCP套接字就會始終處在CLOSE_WAIT狀態(tài)。進程/線程長時間持有不需要的socket描述符,更多的并不是有意的,而是在進行諸如fork/clone之類的系統(tǒng)調(diào)用后,dup了父親的文件描述符,然后在孩子那里又沒有及時關閉,另外的原因就是編程者對socket描述符的close接口以及shutdown接口不是很理解了。
現(xiàn)在,我們用一個問題來繼續(xù)我們的討論。
什么時候進程在超長的生命周期內(nèi)不會如愿關閉TCP從而發(fā)送FIN呢?
我的答案比較直接:不能指望close會發(fā)送FIN!
相信很多人在想斷開一個TCP連接的時候,都會調(diào)用close吧。并且這種做法幾乎都是正確的,以至于很多人都把這作為一種標準的做法。但是這是不對的!Why?!在《UNIX網(wǎng)絡編程》中,曾經(jīng)提到了所謂的”優(yōu)雅關閉TCP連接“,何謂優(yōu)雅??!如果你充分理解close,shutdown,應該就會知道,CLOSE_WAIT出現(xiàn),你應該可以給出一些解釋。
close調(diào)用
close的參數(shù)只是一個文件描述符號,它不理解這個文件真正的細節(jié),它只是一個文件系統(tǒng)內(nèi)范疇的一個調(diào)用,它只是關閉文件描述符,保證此進程不會在讀取它而已。如果你關閉了文件描述符4,即close(4),你知道4代表的文件會作何反應嗎??文件系統(tǒng)并不知道4號描述符代表的文件到底是什么,更不知道有多少進程共享這個底層的”實體“,所以一個進程層面上邏輯根本沒有權(quán)力去徹底關閉一個socket。如果你想了解close的細節(jié),更應該去看看UNIX文件抽象或者文件系統(tǒng)的細節(jié),而不是socket。請參見位于fs/open.c中的:
SYSCALL_DEFINE1(close, unsigned int, fd)
{
...
fdt = files_fdtable(files);
...
filp = fdt- >fd[fd];
...
retval = filp_close(filp, files);
...
return retval;
...
}
EXPORT_SYMBOL(sys_close);
在filp_close中會有fput調(diào)用:
void fput(struct file *file)
{
if (atomic_long_dec_and_test(&file- >f_count))
__fput(file);
}
看到那個引用計數(shù)了嗎?只有當這個文件的引用計數(shù)變成0的時候,才會調(diào)用底層的關閉邏輯,對于socket而言,如果仍然還有一個進程或者線程持有這個socket對應的文件系統(tǒng)的描述符,那么即便你調(diào)用了close,也不會進入了socket的close邏輯,它在文件系統(tǒng)層面就返回了!
shutdown調(diào)用
這個才是真正關閉一個TCP連接的調(diào)用!shutdown并沒有文件系統(tǒng)的語義,它專門針對內(nèi)核層的TCP socket。因此,調(diào)用shutdown的邏輯,才是真正關閉了與之共享信道的tcp socket。
所謂的優(yōu)雅關閉,就是在調(diào)用close之前, 首先自己調(diào)用shutdown(RD or WD)。這樣的時序才是關閉TCP的必由之路!
如果你想優(yōu)雅關閉一個TCP連接,請先用shutdown,然后后面跟一個close。不過有點詭異的是,Linux的shutdown(SHUT_RD)貌似沒有任何效果,不過這無所謂了,本來對于讀不讀的,就不屬于TCP的范疇,只有SHUT_WR才會實際發(fā)送一個FIN給對方。
-
數(shù)據(jù)
+關注
關注
8文章
7256瀏覽量
91863 -
代碼
+關注
關注
30文章
4900瀏覽量
70735 -
腳本
+關注
關注
1文章
398瀏覽量
28454
發(fā)布評論請先 登錄
CC3200 socket狀態(tài)如何查詢?
是否有一些命令知道LAN820板的狀態(tài)?
關閉34908A繼電器并保持關閉狀態(tài)
UC3901/UC2901/UC1901 pdf datas
水電機組的狀態(tài)監(jiān)測及狀態(tài)檢修
狀態(tài)機舉例
MC33972抑制喚醒多路開關檢測接口

基于設備狀態(tài)的網(wǎng)絡狀態(tài)評估方案
狀態(tài)模式(狀態(tài)機)

linux 中 ACPI 電源管理 G 狀態(tài)、S 狀態(tài)、D 狀態(tài)、C 狀態(tài)、P 狀態(tài)

評論