,笔者一直觉得,从应用程序到框架再到操作系统的每一段代码都懂,会是一件激动人心的事情。上一篇博客讲了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(offset
