当前位置: 首页 > 科技观察

从Linux源码看Socket的Close

时间:2023-03-12 17:42:34 科技观察

,笔者一直觉得,从应用程序到框架再到操作系统的每一段代码都懂,会是一件激动人心的事情。上一篇博客讲了socket阻塞和非阻塞,本文开始讲socketclose(以tcp为例,基于linux-2.6.24内核版本)TCPclose状态转换图:众所周知,TCP的关闭过程就是四次挥手,状态机的转换逃不过TCP状态转换图,如下图:tcp的关闭主要分为主动关闭、被动关闭和同时关闭(特殊情况,不做说明)主动关闭close(fd)过程以C语言为例。我们在关闭socket的时候,会用到close(fd)函数:intsocket_fd;socket_fd=socket(AF_INET,SOCK_STREAM,0);...//通过这里的文件描述符close(socket_fd)和close(int)关闭对应的socketclosefd)是通过系统调用sys_close执行的:asmlinkagelongsys_close(unsignedintfd){//清除(close_on_exec是退出进程时)位图标记FD_CLR(fd,fdt->close_on_exec);//释放文件描述符//清除相应位在fdt打开的fd位图中->open_fds//然后把fd挂载到下一个可用的fd中去重用__put_unused_fd(files,fd);//调用file_pointer的close方法真正清除retval=filp_close(filp,files);}我们看到最后的filp_close方法调用:intfilp_close(structfile*filp,fl_owner_tid){//如果有flush方法,flushif(filp->f_op&&filp->f_op->flush)filp->f_op->flush(filp,id);//调用fputfput(filp);......}然后我们输入fput:voidfastcallfput(structfile*file){//对应的file->count--,同时检查是否有是对这个文件的引用//如果不是,调用_fput释放if(atomic_dec_and_test(&file->f_count))__fput(file);}同一个文件很常见的是(socket)有多个引用,比如下面这个例子:所以在写一个多进程socketserver的过程中,父进程也需要close(fd)一次,这样就无法关闭socketfinally然后是_fput函数:voidfastcall__fput(structfile*file){//releasefilefromeventpolleventpoll_release(file);//如果是release方法,调用releaseif(file->f_op&&file->f_op->release)file->f_op->release(inode,file);}因为我们讨论的是socket的close,所以我们现在探索socket情况下file->f_op->release的实现:f_op->release的赋值我们追溯创建socket的代码,即socket(AF_INET,SOCK_STREAM,0);|-sock_create//创建sock|-sock_map_fd//关联sock和fd|-sock_attach_fd|-init_file(file,...,&socket_file_ops);|-file->f_op=fop;//fop赋值给socket_file_opssocket_file_ops的实现是:staticconststructfile_operationssocket_file_ops={.owner=THIS_MODULE,...//我们只考虑sock_close.release=sock_close,...};继续跟踪:sock_close|-sock_release|-sock->ops->release(sock);在上一篇博客中,我们知道sock->ops如下图所示:即(这里只考虑tcp,即sk_prot=tcp_prot):inet_stream_ops->release|-inet_release|-sk->sk_prot->close(sk,超时);|-tcp_prot->关闭(sk,超时);|->tcp_prot.tcp_close关于fd和socket的关系如下图所示:上图中红线为close(fd)的调用链tcp_closevoidtcp_close(structsock*sk,longtimeout){if(sk->sk_state==TCP_LISTEN){//如果是监听状态,直接设置为关闭状态(sk)){//tcp_close_state将sk从已建立状态变为fin_wait1//发送fin包tcp_send_fin(sk);}......}四次挥手现在就是我们的四次挥手,其中前半部分的两个挥手如下图所示:首先,在tcp_close_state(sk)中状态已经设置为fin_wait1,调用tcp_send_finvoidtcp_send_fin(structsock*sk){...//这里设置flags为ack和finTCP_SKB_CB(skb)->flags=(TCPB_FLAG_ACK|TCPB_FLAG_FIN);......//发送fin包并关闭nagle__tcp_push_pending_frames(sk,mss_now,TCP_NAGLE_OFF);}如上Step1所示然后,主动关闭的一端等待另一端的ACK。如果ACK回来了,设置TCP状态为FIN_WAIT2,如上图步骤2所示。具体代码如下:,unsignedlen){.../*step5:checktheACKfield*/if(th->ack){...caseTCP_FIN_WAIT1://这里的判断是为了确认这个ack是对应的ackif(tp->snd_una==tp->write_seq){//设置为FIN_WAIT2状态tcp_set_state(sk,TCP_FIN_WAIT2);...//设置TCP_FIN_WAIT2定时器,tmo时间到后状态变为TIME_WAIT//但是这时候变化已经是inet_timewait_socktcp_time_wait(sk,TCP_FIN_WAIT2,tmo);...}}/*step7:processthesegmenttext*/switch(sk->sk_state){caseTCP_FIN_WAIT1:caseTCP_FIN_WAIT2:......caseTCP_ESTABLISHED:tcp_data_queue(sk,skb);queued=1;break;}.....}注意的值是,在从TCP_FIN_WAIT1过渡到TCP_FIN_WAIT2之后,还会调用tcp_time_wait设置一个TCP_FIN_WAIT2定时器,在tmo+(2MSL或基于RTO计算超时)超时后会直接转为关闭状态(但此时已经是inet_timewait_sock)。这个超时时间是可以配置的。如果是ipv4,可以这样配置:net.ipv4.tcp_fin_timeout/sbin/sysctl-wnet.ipv4.tcp_fin_timeout=30如下图:之所以做这一步是为了防止对端的原因就是没有发送fin,防止一直处于FIN_WAIT2状态。然后在FIN_WAIT2状态等待对端的FIN,完成接下来的两次挥手:通过Step1和Step2设置状态为FIN_WAIT_2,然后在收到对端发送的FIN后设置状态为time_wait,如图在以下代码中:tcp_v4_do_rcv|-tcp_rcv_state_process|-tcp_data_queue|-tcp_finstaticvoidtcp_fin(structsk_buff*skb,structsock*sk,structtcphdr*th){switch(sk->sk_state){...caseTCP_FIN_WAIT1://这里是同时关闭的情况tcp_send_ack(sk);tcp_set_state(sk,TCP_CLOSING);break;caseTCP_FIN_WAIT2:/*ReceivedaFIN--sendACKandenterTIME_WAIT.*///收到FIN后,发送ACK进入TIME_WAIT状态tcp_send_ack(sk);tcp_time_wait(sk,TCP_TIME_WAIT,0);}}在time_wait状态下,原来的socket会被销毁,然后创建一个新的inet_timewait_sock,以便及时回收原来socket使用的资源。inet_timewait_sock挂在一个bucket中,time_wait超过(2MSL或根据RTO计算的时间)的实例被inet_twdr_twcal_tick定时从bucket中删除。我们看一下tcp_time_wait函数voidtcp_time_wait(structsock*sk,intstate,inttimeo){//建立inet_timewait_socktw=inet_twsk_alloc(sk,state);//放到bucket的具体位置等待定时器删除inet_twsk_schedule(tw,&tcp_death_row,time,TCP_TIMEWAIT_LEN);//设置sk状态为TCP_CLOSE,然后回收sk资源tcpsocket,如果处于established状态,收到对端的FIN后,处于passiveshutdown状态,会进入close_wait状态,如下图Step1所示:具体代码如下如下:tcp_rcv_state_process|-tcp_data_queuestaticvoidtcp_data_queue(structsock*sk,structsk_buff*skb){...if(th->fin)tcp_fin(skb,sk,th);...}我们看一下tcp_finstaticvoidtcp_fin(structsock_buff*skb,structsock*sk,structtcphdr*th){...//这句话在表示当前socket有ack需要发送inet_csk_schedule_ack(sk);......switch(sk->sk_state){caseTCP_SYN_RECV:caseTCP_ESTABLISHED:/*MovetoCLOSE_WAIT*///state设置程序close_waitstatetcp_set_state(sk,TCP_CLOSE_WAIT);//这句话说明,当前的fin可以延时发送//也就是跟下面的数据一起发送或者定时器超时后再发送inet_csk(sk)->icsk_ack.pingpong=1;break;}......}这里比较有意思的一点是,收到对端的fin后,并不会马上发送ack通知对端已经收到,而是等待发送一条数据,或者等待携带重传计时器在发送ack之前到期。如果对端关闭,应用端读取时得到的返回值为0,则需要手动调用close关闭连接if(recv(sockfd,buf,MAXLINE,0)==0){close(sockfd)}我们看看recv是如何处理fin包的,从而返回0。之前的博客显示recv在最后调用了tcp_rcvmsg。因为比较复杂,所以分两节看:第一节tcp_recvmsg...//从接收队列中获取一个sk_bufferskb=skb_peek(&sk->sk_receive_queue);do{//如果没有数据,直接跳出读取循环返回0if(!skb)break;......//*seq表示已经读了多少seq//TCP_SKB_CB(skb)->seq表示当前的起始seqsk_buffer//offset为当前sk_buffer中已经读取的长度offset=*seq-TCP_SKB_CB(skb)->seq;//syn处理if(tcp_hdr(skb)->syn)offset--;//这里的判断表明即当前skb还有数据要读,跳转到found_ok_skbif(offsetlen)gotofound_ok_skb;//处理fin包情况//offset==skb->len,跳转到found_fin_ok然后跳出big循环//并返回0if(tcp_hdr(skb)->fin)gotofound_fin_ok;BUG_TRAP(flags&MSG_PEEK);skb=skb->next;}while(skb!=(structsk_buff*)&sk->sk_receive_queue);......上面代码的流程如下图所示:tcp_recmsg第二段:found_ok_skb://tcpreadsequpdate*seq+=used;//本次读取的数量更新copied+=used;//如果当前sk_buffer的末尾还没有读到,fin标志不会被检测到if(used+offsetlen)continue;//如果发现当前skb有fin标志,则去found_fin_okif(tcp_hdr(skb)->fin)gotofound_fin_ok;......found_fin_ok:/*ProcesstheFIN.*///tcphasreadseq++++*seq;...break;}while(len>0);从上面的代码可以看出,一旦当前skb读取完毕并带有fin标志时,无论是否读取到用户期望的字节数,都会返回读取到的字节数。取任意数据,跳转到found_fin_ok,返回0。这样,应用程序可以感知到peer已经关闭。如下图:last_ack应用层发现对端关闭后已经处于close_wait状态。此时如果再次调用close,状态会变为last_ack状态,发送本端的fin,如下代码所示:voidtcp_close(structsock*sk,longtimeout){...elseif(tcp_close_state(sk)){//tcp_close_state会将sk从close_wait状态变为last_ack//发送fin包tcp_send_fin(sk);}}收到主动关闭端的last_ack后,再调用tcp_done(sk)设置sk到tcp_closed状态,并回收sk的资源,如下代码所示:/*step5:checktheACKfield*/if(th->ack){...caseTCP_LAST_ACK://这里的判断是为了确认这个ack是ackif(tp->snd_una==tp->write_seq){tcp_update_metrics(sk);//设置socket关闭,回收socket资源tcp_done(sk);gotodiscard;}...}}以上代码就是passiveclosingend的最后两波,如下图:tion在检测到peerfin时没有及时关闭当前连接。有一种可能如下图:出现这种情况,一般是因为minIdle等参数配置不正确(如果连接池有定时收缩连接的功能)。在连接池中加入心跳也可以解决这个问题。如果应用程序关闭时间太晚,则对等方已经破坏了连接。然后应用向对端发送fin,对端由于找不到对应的连接,发送RST(Reset)报文。操作系统什么时候回收close_wait?如果应用程序长时间没有调用close_wait,操作系统是否有回收机制?答案是肯定的。TCP本身有一个包活(keepalive)定时器,在(keepalive)定时器超时后,连接会被强行关闭。可以设置tcpkeepalive的时间/etc/sysctl.confnet.ipv4.tcp_keepalive_intvl=75net.ipv4.tcp_keepalive_probes=9net.ipv4.tcp_keepalive_time=7200默认值如上图,设置很大,会7200秒后超时。如果想快速回收close_wait可以设置小一些。但最终的解决方案还是要从应用开始。关于tcpkeepalivepacketlivetimer,见作者的另一篇博客:https://my.oschina.net/alchemystar/blog/833981进程关闭时,进程退出时清理socket资源(是否kill,kill-9ornormalexit)会关闭当前进程中的所有fd(文件描述符)send_finJavaGC清理每一个是socket的fdsocket资源Javasocket最终关联到AbstractPlainSocketImpl,重写了对象的finalize方法}......}所以Java会在GC的时候关闭未引用的socket,但是切记不要依赖Java的GC,因为GC的时间不是根据未引用的socket的数量来判断的,所以可能会泄露一堆socket,但是仍然没有GC被触发。综上所述,linux内核的源代码博大精深,阅读其代码费了很大的功夫。之前看\<\>的时候,由于有前辈的指导和整理,阅读书中使用的BSD源码并没有觉得很吃力。直到现在,带着疑问独立看linux源码,尽管有了之前的基础,还是被各种细节弄得一头雾水。希望作者的这篇文章能够对阅读linux网络协议栈代码的人有所帮助。原文链接https://my.oschina.net/alchemystar/blog/1821680本文转载自微信公众号《Bug解决之路》,可通过以下二维码关注。转载本文请联系BUG解决公众号。