關(guān)于關(guān)閉一個(gè)還有沒發(fā)送數(shù)據(jù)完的TCP連接思考
背景
有一次,光神?在群問了個(gè)問題:
當(dāng) close 一個(gè) TCP 連接時(shí),如果還有沒發(fā)送完的數(shù)據(jù)在緩沖區(qū)中,內(nèi)核會(huì)怎么處理?
當(dāng)時(shí)我認(rèn)為,因?yàn)殛P(guān)閉 TCP 連接會(huì)觸發(fā)四次揮手過程,而為了讓四次揮手能夠快速完成,應(yīng)該會(huì)把發(fā)送緩沖區(qū)的數(shù)據(jù)清空,然后發(fā)送四次揮手的數(shù)據(jù)包。
帶著疑問,我去查閱 Linux 源碼的實(shí)現(xiàn),下面就是關(guān)閉一個(gè) TCP 連接的過程。
關(guān)閉 TCP 連接過程
關(guān)閉一個(gè) TCP 連接可以使用?close()?系統(tǒng)調(diào)用,我們來分析一下當(dāng)調(diào)用?close()?關(guān)閉一個(gè) TCP 連接時(shí)會(huì)發(fā)生什么事情。
當(dāng)調(diào)用?close()?系統(tǒng)調(diào)用時(shí),會(huì)觸發(fā)調(diào)用?sys_close()?內(nèi)核函數(shù),其實(shí)現(xiàn)如下:
asmlinkage long sys_close(unsigned int fd){struct file * filp;struct files_struct *files = current->files;...return filp_close(filp, files);...}
sys_close()?函數(shù)最終會(huì)調(diào)用?file_close()?函數(shù)來關(guān)閉文件(由于在 Linux 中 socket 是一種特殊的文件),我們接著分析?filp_close()?函數(shù)的實(shí)現(xiàn):
int filp_close(struct file *filp, fl_owner_t id){...fput(filp);return retval;}void fput(struct file * file){...if (atomic_dec_and_test(&file->f_count)) {...if (file->f_op && file->f_op->release)file->f_op->release(inode, file);...}}
可以看到,最終會(huì)調(diào)用文件系統(tǒng)對(duì)應(yīng)的?release()?方法來處理關(guān)閉操作。對(duì)于 socket 文件系統(tǒng),release()?方法對(duì)應(yīng)的是?sock_close()?函數(shù),而?sock_close()?函數(shù)最終會(huì)調(diào)用?sock_release()?函數(shù),所以我們來看看?sock_release()?函數(shù)的實(shí)現(xiàn):
void sock_release(struct socket *sock){if (sock->ops)sock->ops->release(sock);...}
sock_release()?函數(shù)也很簡單,就是調(diào)用對(duì)應(yīng)?協(xié)議族?的?release()?方法,因?yàn)?Linux 的 socket 文件系統(tǒng)可以支持多種協(xié)議族,比如?INET、Unix Domain Socket、Netlink?等。而對(duì)應(yīng)?INET協(xié)議族(網(wǎng)絡(luò))?來說,這個(gè)?release()?方法對(duì)應(yīng)的是?inet_release()?函數(shù),inet_release()?函數(shù)實(shí)現(xiàn)如下:
int inet_release(struct socket *sock){struct sock *sk = sock->sk;if (sk) {long timeout;...timeout = 0;if (sk->linger && !(current->flags & PF_EXITING))timeout = sk->lingertime;sock->sk = NULL;sk->prot->close(sk, timeout);}return(0);}
inet_release()?函數(shù)最終會(huì)調(diào)用對(duì)應(yīng)?傳輸層(TCP或者UDP)?的?close()?方法,對(duì)于?TCP協(xié)議?來說,close()?方法對(duì)應(yīng)的是?tcp_close()?函數(shù),tcp_close()?就是關(guān)閉 TCP 連接的最后站點(diǎn)。
由于?tcp_close()?函數(shù)比較復(fù)雜,我們這里只分析當(dāng)發(fā)生緩沖區(qū)還有數(shù)據(jù)的情況下,內(nèi)核會(huì)怎么處理緩沖區(qū)的數(shù)據(jù)。
void tcp_close(struct sock *sk, long timeout){struct sk_buff *skb;int data_was_unread = 0;...// 如果接收緩沖區(qū)有數(shù)據(jù), 那么先情況接收緩沖區(qū)的數(shù)據(jù)while((skb= __skb_dequeue(&sk->receive_queue)) != NULL) {u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq - skb->h.th->fin;data_was_unread += len;__kfree_skb(skb);}...if (data_was_unread != 0) { // 如果接收緩沖區(qū)有數(shù)據(jù)沒有處理tcp_set_state(sk, TCP_CLOSE); // 把socket狀態(tài)設(shè)置為TCP_CLOSEtcp_send_active_reset(sk, GFP_KERNEL); // 發(fā)送一個(gè)reset包給對(duì)端連接} else if (sk->linger && sk->lingertime==0) {...} else if (tcp_close_state(sk)) {tcp_send_fin(sk); // 開始發(fā)生四次揮手包}...}
從?tcp_close()?函數(shù)的實(shí)現(xiàn)可以看出,關(guān)閉過程主要有兩種情況:
如果接收緩沖區(qū)還有數(shù)據(jù)沒有被用戶處理,那么就先把接收緩沖區(qū)的數(shù)據(jù)清空,并且發(fā)送一個(gè) reset 包給對(duì)端連接。
如果接收緩沖區(qū)沒有數(shù)據(jù),那么就調(diào)用?
tcp_send_fin()?函數(shù)開始進(jìn)行四次揮手過程。
四次揮手過程如下圖:

接下來,我們分析?tcp_send_fin()?函數(shù)的實(shí)現(xiàn):
void tcp_send_fin(struct sock *sk){struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp);struct sk_buff *skb = skb_peek_tail(&sk->write_queue); // 發(fā)送緩沖區(qū)列表最后一個(gè)緩沖塊unsigned int mss_now;...if (tp->send_head != NULL) { // 如果發(fā)送緩沖區(qū)不為空TCP_SKB_CB(skb)->flags |= TCPCB_FLAG_FIN; // 把最后一個(gè)發(fā)送緩沖塊設(shè)置FIN標(biāo)志TCP_SKB_CB(skb)->end_seq++;tp->write_seq++;} else { // 如果發(fā)送緩沖區(qū)為空for (;;) {skb = alloc_skb(MAX_TCP_HEADER, GFP_KERNEL); // 申請(qǐng)一個(gè)新的緩沖塊if (skb)break;current->policy |= SCHED_YIELD;schedule();}skb_reserve(skb, MAX_TCP_HEADER);skb->csum = 0;TCP_SKB_CB(skb)->flags = (TCPCB_FLAG_ACK | TCPCB_FLAG_FIN); // 設(shè)置FIN標(biāo)志TCP_SKB_CB(skb)->sacked = 0;TCP_SKB_CB(skb)->seq = tp->write_seq;TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(skb)->seq + 1;tcp_send_skb(sk, skb, 1, mss_now); // 發(fā)送給對(duì)端連接}...}
在?tcp_send_fin()?函數(shù)我們終于找到了當(dāng)發(fā)送緩沖區(qū)不為空的處理,當(dāng)發(fā)送緩沖區(qū)不為空時(shí),首先會(huì)獲取發(fā)送緩沖區(qū)的最后一個(gè)緩沖塊,然后把這個(gè)緩沖區(qū)的?FIN標(biāo)志位?設(shè)置上。
所以我前面的想法是錯(cuò)的,當(dāng)關(guān)閉一個(gè) TCP 連接時(shí),如果發(fā)送緩沖區(qū)還有數(shù)據(jù)沒發(fā)送完,那么內(nèi)核只會(huì)把發(fā)送緩沖區(qū)最后一個(gè)緩沖塊設(shè)置上?FIN標(biāo)志,而不是把發(fā)送緩沖區(qū)清空。
