3

「linux」关于关闭一个还有没发送数据完的TCP连接思考

 3 years ago
source link: https://studygolang.com/articles/33714
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

tcp相关视频解析:

tcp专题训练营之深度解析tcp/ip协议栈

徒手实现网络协议栈,请准备好环境,一起来写代码

linux后台开发面试中tcpip,哪些容易被问到的

当 close 一个 TCP 连接时,如果还有没发送完的数据在缓冲区中,内核会怎么处理?

当时我认为,因为关闭 TCP 连接会触发四次挥手过程,而为了让四次挥手能够快速完成,应该会把发送缓冲区的数据清空,然后发送四次挥手的数据包。

带着疑问,我去查阅 Linux 源码的实现,下面就是关闭一个 TCP 连接的过程。

关闭 TCP 连接过程

关闭一个 TCP 连接可以使用 close() 系统调用,我们来分析一下当调用 close() 关闭一个 TCP 连接时会发生什么事情。

当调用 close() 系统调用时,会触发调用 sys_close() 内核函数,其实现如下:

asmlinkagelongsys_close(unsignedintfd){structfile*filp;structfiles_struct*files=current->files;...returnfilp_close(filp, files);    ...}

sys_close() 函数最终会调用 file_close() 函数来关闭文件(由于在 Linux 中 socket 是一种特殊的文件),我们接着分析 filp_close() 函数的实现:

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);        ...    }}

可以看到,最终会调用文件系统对应的 release() 方法来处理关闭操作。对于 socket 文件系统,release() 方法对应的是 sock_close() 函数,而 sock_close() 函数最终会调用 sock_release() 函数,所以我们来看看 sock_release() 函数的实现:

void sock_release(structsocket*sock){if(sock->ops)        sock->ops->release(sock);    ...}

sock_release() 函数也很简单,就是调用对应 协议族 的 release() 方法,因为 Linux 的 socket 文件系统可以支持多种协议族,比如 INET、Unix Domain Socket、Netlink 等。而对应 INET协议族(网络) 来说,这个 release() 方法对应的是 inet_release() 函数,inet_release() 函数实现如下:

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() 函数最终会调用对应 传输层(TCP或者UDP) 的 close() 方法,对于 TCP协议来说,close() 方法对应的是 tcp_close() 函数,tcp_close() 就是关闭 TCP 连接的最后站点。

【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

7B3u6vI.png!mobile

由于 tcp_close() 函数比较复杂,我们这里只分析当发生缓冲区还有数据的情况下,内核会怎么处理缓冲区的数据。

void tcp_close(structsock*sk, long timeout){structsk_buff*skb;    int data_was_unread =0;    ...// 如果接收缓冲区有数据, 那么先情况接收缓冲区的数据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) {// 如果接收缓冲区有数据没有处理tcp_set_state(sk, TCP_CLOSE);// 把socket状态设置为TCP_CLOSEtcp_send_active_reset(sk, GFP_KERNEL);// 发送一个reset包给对端连接}elseif(sk->linger && sk->lingertime==0) {        ...    }elseif(tcp_close_state(sk)) {        tcp_send_fin(sk);// 开始发生四次挥手包}    ...}

从 tcp_close() 函数的实现可以看出,关闭过程主要有两种情况:

如果接收缓冲区还有数据没有被用户处理,那么就先把接收缓冲区的数据清空,并且发送一个 reset 包给对端连接。

如果接收缓冲区没有数据,那么就调用 tcp_send_fin() 函数开始进行四次挥手过程。

四次挥手过程如下图:

jAFrQv3.jpg!mobile

接下来,我们分析 tcp_send_fin() 函数的实现:

void tcp_send_fin(structsock*sk){structtcp_opt*tp = &(sk->tp_pinfo.af_tcp);structsk_buff*skb = skb_peek_tail(&sk->write_queue);// 发送缓冲区列表最后一个缓冲块unsigned int mss_now;    ...if(tp->send_head != NULL) {// 如果发送缓冲区不为空TCP_SKB_CB(skb)->flags |= TCPCB_FLAG_FIN;// 把最后一个发送缓冲块设置FIN标志TCP_SKB_CB(skb)->end_seq++;        tp->write_seq++;    }else{// 如果发送缓冲区为空for(;;) {            skb = alloc_skb(MAX_TCP_HEADER, GFP_KERNEL);// 申请一个新的缓冲块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);// 设置FIN标志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);// 发送给对端连接}    ...}

在 tcp_send_fin() 函数我们终于找到了当发送缓冲区不为空的处理,当发送缓冲区不为空时,首先会获取发送缓冲区的最后一个缓冲块,然后把这个缓冲区的 FIN标志位 设置上。

所以我前面的想法是错的,当关闭一个 TCP 连接时,如果发送缓冲区还有数据没发送完,那么内核只会把发送缓冲区最后一个缓冲块设置上 FIN标志,而不是把发送缓冲区清空。

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK