tcp相關(guān)視頻解析:
tcp專題訓(xùn)練營(yíng)之深度解析tcp/ip協(xié)議棧
徒手實(shí)現(xiàn)網(wǎng)絡(luò)協(xié)議棧反璃,請(qǐng)準(zhǔn)備好環(huán)境墓陈,一起來寫代碼
linux后臺(tái)開發(fā)面試中tcpip,哪些容易被問到的
當(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)如下:
asmlinkagelongsys_close(unsignedintfd){structfile*filp;structfiles_struct*files=current->files;...returnfilp_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(structfile*filp, fl_owner_t id){? ? ...? ? fput(filp);returnretval;}void fput(structfile* 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(structsocket*sock){if(sock->ops)? ? ? ? sock->ops->release(sock);? ? ...}
sock_release()?函數(shù)也很簡(jiǎn)單,就是調(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(structsocket*sock){structsock*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)摇肌。
【文章福利】需要C/C++ Linux服務(wù)器架構(gòu)師學(xué)習(xí)資料加群812855908(資料包括C/C++,Linux仪际,golang技術(shù),Nginx昵骤,ZeroMQ树碱,MySQL,Redis变秦,fastdfs成榜,MongoDB,ZK蹦玫,流媒體赎婚,CDN刘绣,P2P,K8S挣输,Docker纬凤,TCP/IP,協(xié)程撩嚼,DPDK停士,ffmpeg等)
由于?tcp_close()?函數(shù)比較復(fù)雜,我們這里只分析當(dāng)發(fā)生緩沖區(qū)還有數(shù)據(jù)的情況下完丽,內(nèi)核會(huì)怎么處理緩沖區(qū)的數(shù)據(jù)恋技。
void tcp_close(structsock*sk, long timeout){structsk_buff*skb;? ? int data_was_unread =0;? ? ...// 如果接收緩沖區(qū)有數(shù)據(jù), 那么先情況接收緩沖區(qū)的數(shù)據(jù)while((skb= __skb_dequeue(&sk->receive_queue)) != NULL) {u32len = 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ì)端連接}elseif(sk->linger && sk->lingertime==0) {? ? ? ? ...? ? }elseif(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(structsock*sk){structtcp_opt*tp = &(sk->tp_pinfo.af_tcp);structsk_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ū)清空。