TCP連接的關閉
阿新 • • 發佈:2017-09-09
works init -h 得到 客戶端 可靠的 name 可靠性 tex 原文地址:http://lib.csdn.net/article/computernetworks/17264
TCP連接的關閉有兩個方法close和shutdown,這篇文章將盡量精簡的說明它們分別做了些什麽。
為方便閱讀,我們可以帶著以下5個問題來閱讀本文:
1、當socket被多進程或者多線程共享時,關閉連接時有何區別?
2、關連接時,若連接上有來自對端的還未處理的消息,會怎麽處理?
3、關連接時,若連接上有本進程待發送卻未來得及發送出的消息,又會怎麽處理?
4、so_linger這個功能的用處在哪?
5、對於監聽socket執行關閉,和對處於ESTABLISH這種通訊的socket執行關閉,有何區別?
下面分三部分進行:首先說說多線程多進程關閉連接的區別;再用一幅流程圖談談close;最後用一幅流程圖說說shutdown。
先不提其原理和實現,從多進程、多線程下 close和shutdown方法調用時的區別說起。
看看close與shutdown這兩個系統調用對應的內核函數:(參見unistd.h文件)
但sys_close和sys_shutdown這兩個系統調用最終是由tcp_close和tcp_shutdown方法來實現的,調用過程如下圖所示: sys_shutdown與多線程和多進程都沒有任何關系,而sys_close則不然,上圖中可以看到,層層封裝調用中有一個方法叫fput,它有一個引用計數,記錄這個socket被引用了多少次。在說明多線程或者多進程調用close的區別前,先在代碼上簡單看下close是怎麽調用的,對內核代碼沒興趣的同學可以僅看fput方法:
當這個socket的引用計數f_count不為0時,是不會觸發到真正關閉TCP連接的tcp_close方法的。 那麽,這個引用計數的意義何在呢?為了說明它,先要說道下進程與線程的區別。 大家知道,所謂線程其實就是“輕量級”的進程。創建進程只能是一個進程(父進程)創建另一個進程(子進程),子進程會復制父進程的資源,這裏的”復制“針對不同的資源其意義是不同的,例如對內存、文件、TCP連接等。創建進程是由clone系統調用實現的,而創建線程時同樣也是clone實現的,只不過clone的參數不同,其行為也很不同。這個話題是很大的,這裏我們僅討論下TCP連接。 在clone系統調用中,會調用方法copy_files來拷貝文件描述符(包括socket)。創建線程時,傳入的flag參數中包含標誌位CLONE_FILES,此時,線程將會共享父進程中的文件描述符。而創建進程時沒有這個標誌位,這時,會把進程打開的所有文件描述符的引用計數加1,即把file數據結構的f_count成員加1,如下:
再看看dup_fd方法:
get_file宏就會加引用計數。
所以,子進程會將父進程中已經建立的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個步驟,其含義為:客戶端打開接收、發送的功能;服務器端認可並也打開接收、發送的功能;客戶端認可。當第1、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。
#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))//檢查引用計數,直到為0才會真正去關閉socket __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個步驟,其含義為:客戶端打開接收、發送的功能;服務器端認可並也打開接收、發送的功能;客戶端認可。當第1、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。
TCP連接的關閉