Close行為:
當應用程序在調用close()函數關閉TCP連接時,Linux內核的默認行為是將套接口發送隊列里的原有數據(比如之前殘留的數據)以及新加入 的數據(比如函數close()產生的FIN標記,如果發送隊列沒有殘留之前的數據,那么這個FIN標記將單獨產生一個新數據包)發送出去並且銷毀套接口 (並非把相關資源全部釋放,比如只是把內核對象sock標記為dead狀態等,這樣當函數close()返回后,TCP發送隊列的數據包仍然可以繼續由內 核協議棧發送,但是一些相關操作就會受到影響和限制,比如對數據包發送失敗后的重傳次數)后立即返回。這需要知道兩點:
第一,當應用程序獲得 close()函數的返回值時,待發送的數據可能還處在Linux內核的TCP發送隊列里,因為當我們調用write()函數成功寫出數據時,僅表示這些 數據被Linux內核接收放入到發送隊列,如果此時立即調用close()函數返回后,那么剛才write()的數據限於TCP本身的擁塞控制機制(比如 發送窗口、接收窗口等等),完全有可能還呆在TCP發送隊列里而未被發送出去;當然也有可能發送出去一些,畢竟在調用函數close()時,進入到 Linux內核后有一次數據包的主動發送機會,即:
tcp_close() -> tcp_send_fin() -> __tcp_push_pending_frames() -> tcp_write_xmit()
第二,所有這些數據的發送,最終卻並不一定能全部被對端確認(即數據包到了對端TCP協議棧的接收隊列),只能做到TCP協議本身提供的一定程度的 保證,比如TCP協議的重傳機制(並且受close()函數影響,重傳機制弱化,也就是如果出現類似系統資源不足這樣的問題,調用過close()函數進 行關閉的套接口所對應的這些數據會優先丟棄)等,因為如果網絡不好可能導致TCP協議放棄繼續重傳或在意外收到對端發送過來的數據時連接被重置導致未成功 發送的數據全部丟失(后面會看到這種情況)。
注意:
當一條TCP連接被多個進程共享時,如果其中一個進程調用close()函數關閉其對應的套接口時,調用到內核里僅僅只是減少對應的引用計數,而不會對 TCP連接做任何關閉操作(即在前面路徑就已經返回了);只有當最后一個進程進行close()關閉時,引用計數變為0時才進行真正的套接口釋放操作(也 即此時才會深調到tcp_close()函數內)。而函數shutdown()不一樣,它是套接口類型描述符所特有的操作,直接作用於套接口連接,根本就 沒有考慮引用計數的影響,這從它的調用路徑就可以基本看出這一點。
SO_LINGER行為:
Linux提供了一個套接口選項SO_LINGER,可以改變在套接口上執行close()函數時的默認行為。選項SO_LINGER用到的相關參數主要是一個linger結構體:
1 2 3 4 5 |
50: Filename : \linux-3.4.4\include\linux\socket.h 51: struct linger { 52: int l_onoff; /* Linger active */ 53: int l_linger; /* How long to linger for */ 54: }; |
注釋很清楚,字段l_onoff標記是否啟用Linger特性,非0為啟用,0為禁用(即內核對close()函數采取默認行為);字段 l_onoff為非0的情況下,字段l_linger生效,如果它的值為0,則導致所有數據丟失且連接立即中止即發送RST;
如果字段l_linger的值為非0(假 定為t秒),那么此時函數close()將被阻塞(假定為阻塞模式)直到:
1) 待發送的數據全部得到了對端確認,返回值為0;
2) 超時返回,返回值為-1,errno被設置為EWOULDBLOCK。
上面兩點是很多介紹TCP/IP協議的經典書(比如Richard Steven的《Unix網絡編程》)上所描述的,但是卻並不適合Linux系統上的實現(《Unix網絡編程》應該是根據BSD上的實現來講的,所以有 些結論不適合Linux系統上的實現,這很正常)。在Linux系統上,應該是函數close()將被阻塞(假定為阻塞模式)直到:
1) 待發送的數據全部得到了對端確認,返回值為0;
2) 發生信號中斷或異常(比如意外收到對端發送過來的數據)或超時,返回值為0;
也就是說,在Linux系統上,針對SO_LINGER選項而言,不論哪種情況,函數close()總是返回0(注意我所針對的情況,我並沒有說在 Linux系統上,函數close()就總是返回0,如果你關閉一個無效的描述符,它同樣也會返回-EBADF的錯誤),並且對於情況2),Linux內核不會清空緩存區,更加不會向對端發送RST數據包(此處指的是發送緩沖區有數據時,而接受緩沖區沒有數據時,一旦接受緩沖區也有數據時就會直接發送RST。原因:tcp連接可以半開半閉,描述符close()掉后,緩存的數據是可以繼續發送出去的,因此“Linux內核不會清空緩存區,更加不會向對端發送RST數 據包”,但對應的描述符不能收數據了(因為close()掉了),所以,如果此時對方發了數據過來就會發送RST以通知對方,如果要發數據過來,必須重置連接。),即執行close()函數的后半部分代碼時不會因此發送任何特別的流程變化(當然,因為 close()函數阻塞了一段時間,在這段時間內,套接口相關字段可能被TCP協議棧修改過了,所以導致相關判斷結果發生變化,但這並不是由於情況2)直 接導致)。你可以說這是Linux內核實現的BUG,但從Linux 2.2+ 開始,它就一直存在,但從未被修復,個人猜測原因有二:第一,基本不會有“通過檢測close()返回值來判斷待發送數據是否發送成功”這種需求,檢測 close()返回值更多的是用來判斷當前關閉的描述符是否有效等;第二,即便判斷出數據沒有發送成功,此時套接口的相關資源已經釋放(當然,也可以實現 對資源先不釋放,但如果這樣完全保留,那么將導致系統不必要的資源浪費),應用程序也無法做出更多補救措施,除了打印一條錯誤日志以外。更重要的是,實現 “判斷待發送數據是否成功發送”的需求有更好的不深度依賴Linux內核的應用層實現方式,即后面將提到的shutdown()函數,至於close() 函數,做好套接口關閉這一單獨的功能就好。所以,即便Linux內核對啟用SO_LINGER選項的套接口調用函數close()的各種情況統一返回0也 並無特別嚴重之處。
那么,在Linux系統上,選項SO_LINGER是否就沒有什么實用的價值了?當然不是,首先,它完全實現了l_onof非0而l_linger 為0情況下的邏輯;其次,它的確阻塞了close()函數,直到待發送的數據全部得到了對端確認或信號中斷、異常、超時返回;在阻塞的這一段時間內,套接 口尚且還處在正常狀態,即此時還沒有打上SOCK_DEAD的標記,因此TCP重傳等各種機制還能平等使用,保證待發送數據發送成功的概率更大。
那么,編寫TCP網絡程序涉及到的一個重要問題凸顯出來了,即:如何盡全力(不可能做到百分之百保證,比如如果網絡斷了,那自然沒法)保證 write()寫出的數據正確的到達對端TCP協議棧的接收隊列而又不被其意外丟棄?如果要求正確到達對端應用層的對應程序,那么自然就需要在應用程序內 做相互確認,而這只適應我們對客戶端和服務器端都可控的情況;對於nginx而言,我們可控的只有服務器端,所以這里不討論這種需求情況。
在Linux系統上,前面已經說明了單獨的選項SO_LINGER對此是無能為力的,所以需要結合選項SO_LINGER、函數close()、函數shutdown()、函數read()做配合設計:
1) 設置SO_LINGER選項參數l_onof非0而l_linger為0;
2) 調用函數shutdown(sock_fd, SHUT_WR);
3) 設置超時定時器,假定為t秒;
4) 調用函數read(sock_fd)阻塞等待,直到讀到EOF或被定時器超時中斷。
5) 執行函數close(sock_fd)或者調用exit(0)進程退出。
這個設計較好的解決了前面討論的使用SO_LINGER選項與close()函數的兩個問題,第一:調用close()關閉套接口時或后,如果接收隊列里 存在有對端發送過來的數據,那么根據文檔RFC 2525,此時需給對端發送一個RST數據包;假定有這樣一種場景(以HTTP的pipelining情況為例,HTTP協議有點特殊,它基本是 request/response的單向數據發送形式,如果是其它交互同時性更強的應用,出現問題的概念更大,但因為我們對HTTP應用比較熟悉,所以就 用它為例以更容易理解):
1) 客戶端應用程序在同一條TCP連接里連續向服務器端發送120個請求(訪問很多門戶網站的首頁時,請求的文件可能還不止這個數目)。
2) 客戶端的所有請求數據順序到達服務器端,服務器端應用程序即開始逐個從內核里讀取請求數據處理並把響應數據通過網絡發回給客戶端。
3) 服務器端應用程序(假定為nginx)限制了在一條連接上只能處理100個請求,因此在處理完第100個請求后結束,調用close()函數關閉連接。
4) 服務器端內核執行對應的tcp_close()函數時發現接收隊列還有請求數據(即請求101-120)因此發送一個RST數據包到客戶端。
5) 客戶端應用程序依次從內核TCP接收隊列讀取服務器端發回的響應數據,但恰好正在讀取第85個請求的響應數據時,客戶端內核收到服務器端的RST數據包,因而丟掉所有接收內容,這包括已被服務器端正常處理了的請求86-100的響應數據。
也就是,上面這種場景下,服務器端write()寫出的數據已經正確的到達對端TCP協議棧的接收隊列,但卻因為服務器端的原因而導致其被意外丟棄。設置SO_LINGER選項是徒勞的,因為在這種情況下,服務器端照樣會發送RST數據包到。
調用函數shutdown(fd, SHUT_WR)是解決第一個問題的關鍵,它僅設置套接口不可寫,即向對端發送一個FIN數據包,表示本端沒有數據需要繼續發送,但是還可以接收數據,所 以此時的套接口對應接收隊列里有數據或后續收到對端發送過來的數據都不會導致服務器端發送RST數據包,避免了客戶端丟棄已正確收到的響應數據。
接着的第二個問題就是:對數據發送是否成功的判斷以及如何對超時連接進行及時釋放?前面闡述了利用close()函數無法達到這個目的,即便輔助使用 SO_LINGER選項。在這里,我們設計等待讓對端先關閉,當然,這個等待是有時限的,所以需設置一個定時器,然后阻塞read(),如果讀到EOF, 也就是對端進行了主動關閉,發送了FIN數據包過來,那么意味着我們發送的數據已經被對端成功接收,此時執行close()函數將會直接關閉:
如果定時器超時(如果是信號中斷,可繼續阻塞read(),直到超時為止),那么說明數據多半沒有發送成功(因為在正常情況下,一旦對端收到我們發送過去 的FIN數據包,即便多做了一些其它處理,它也應該會很快的執行close()進行套接口關閉),在這種情況下,我們執行函數close()進行套接口關 閉,由於SO_LINGER選項設置的影響(參數l_onof非0而l_linger為0),此時將直接發送RST包強行中斷,因為此時的連接已經超時異 常,沒必要再做常規的四次揮手流程,把資源及時釋放更好。
總結:
默認close時,存在兩個問題;
1、 如果在close時,發送緩沖區中有數據還沒有發送出去,本端應用程序無法確認,對端tcp是否能收到(僅由tcp的機制實現安全交付)
2、 如果close時,接收緩沖區還有數據沒有被應用程序讀時,會直接發送RST包到對端,有可能導致對端的應用程序正在讀取tcp緩沖區數據,卻突然收到RST包,導致對端tcp丟棄發送和接收緩沖區里的所有數據,應該程序會丟數據
在linux中so_linger的作用:
它完全實現了l_onof非0而l_linger為0情況下的邏輯(直接發送RST所有數據丟失且連接立即中止);其次,它的確阻塞了close()函數,直到待發送的數據全部得到了對端確認或信號中 斷、異常、超時返回;在阻塞的這一段時間內,套接口尚且還處在正常狀態,即此時還沒有打上SOCK_DEAD的標記,因此TCP重傳等各種機制還能平等使 用,保證待發送數據發送成功的概率更大。但是無論怎么樣返回值都為0,應用程序判斷不出來,具體是那種情況。
正確做法:
在Linux系統上,前面已經說明了單獨的選項SO_LINGER對此是無能為力的,所以需要結合選項SO_LINGER、函數close()、函數shutdown()、函數read()做配合設計:
1) 設置SO_LINGER選項參數l_onof非0而l_linger為0;
2) 調用函數shutdown(sock_fd, SHUT_WR);
3) 設置超時定時器,假定為t秒;
4) 調用函數read(sock_fd)阻塞等待,直到讀到EOF或被定時器超時中斷。
5) 執行函數close(sock_fd)或者調用exit(0)進程退出。
這里有個細節需要注意,如果read是因為讀到對方的fin喚醒后,我們在close fd 僅僅清除fd資源,因為TCP連接雙方已經正常關閉;如果是read超時了而喚醒啦,那么說明數據多半沒有發送成功(因為在正常情況下,一旦對端收到我們發送過去的FIN數據包,即便多做了一些其它處理,它也應該會很快的執行close()進行套接口關閉),在這種情況下,我們執行函數close()進行套接口關閉,由於SO_LINGER選項設置的影響(參數l_onof非0而l_linger為0),此時將直接發送RST包強行中斷,因為此時的連接已經超時異常,沒必要再做常規的四次揮手流程,把資源及時釋放更好。