文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

从Linux源码看Socket的Close

2024-12-03 01:15

关注

笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情。上篇博客讲了socket的阻塞和非阻塞,这篇就开始谈一谈socket的close(以tcp为例且基于linux-2.6.24内核版本)

TCP关闭状态转移图:

众所周知,TCP的close过程是四次挥手,状态机的变迁也逃不出TCP状态转移图,如下图所示:

tcp的关闭主要分主动关闭、被动关闭以及同时关闭(特殊情况,不做描述)

主动关闭

close(fd)的过程

以C语言为例,在我们关闭socket的时候,会使用close(fd)函数:

  1. int    socket_fd; 
  2. socket_fd = socket(AF_INET, SOCK_STREAM, 0); 
  3. ... 
  4. // 此处通过文件描述符关闭对应的socket 
  5. close(socket_fd) 

而close(int fd)又是通过系统调用sys_close来执行的:

  1. asmlinkage long sys_close(unsigned int fd) 
  2.     // 清除(close_on_exec即退出进程时)的位图标记 
  3.     FD_CLR(fd, fdt->close_on_exec); 
  4.     // 释放文件描述符 
  5.     // 将fdt->open_fds即打开的fd位图中对应的位清除 
  6.     // 再将fd挂入下一个可使用的fd以便复用 
  7.     __put_unused_fd(files, fd); 
  8.     // 调用file_pointer的close方法真正清除 
  9.     retval = filp_close(filp, files); 

我们看到最终是调用的filp_close方法:

  1. int filp_close(struct file *filp, fl_owner_t id) 
  2.     // 如果存在flush方法则flush 
  3.     if (filp->f_op && filp->f_op->flush) 
  4.         filp->f_op->flush(filp, id); 
  5.     // 调用fput 
  6.     fput(filp); 
  7.     ...... 

紧接着我们进入fput:

  1. void fastcall fput(struct file *file) 
  2.     // 对应file->count--,同时检查是否还有关于此file的引用 
  3.     // 如果没有,则调用_fput进行释放 
  4.     if (atomic_dec_and_test(&file->f_count)) 
  5.         __fput(file); 

同一个file(socket)有多个引用的情况很常见,例如下面的例子:

所以在多进程的socket服务器编写过程中,父进程也需要close(fd)一次,以免socket无法最终关闭

然后就是_fput函数了:

  1. void fastcall __fput(struct file *file) 
  2.     // 从eventpoll中释放file 
  3.     eventpoll_release(file); 
  4.     // 如果是release方法,则调用release 
  5.     if (file->f_op && file->f_op->release) 
  6.         file->f_op->release(inode, file); 

由于我们讨论的是socket的close,所以,我们现在探查下file->f_op->release在socket情况下的实现:

f_op->release的赋值

我们跟踪创建socket的代码,即

  1. socket(AF_INET, SOCK_STREAM, 0); 
  2.     |-sock_create  // 创建sock 
  3.     |-sock_map_fd  // 将sock和fd关联 
  4.             |-sock_attach_fd 
  5.                     |-init_file(file,...,&socket_file_ops); 
  6.                             |-file->f_op = fop; //fop赋值为socket_file_ops 

socket_file_ops的实现为:

  1. static const struct file_operations socket_file_ops = { 
  2.     .owner =    THIS_MODULE, 
  3.     ...... 
  4.     // 我们在这里只考虑sock_close 
  5.     .release =    sock_close, 
  6.     ...... 
  7. }; 

继续跟踪:

  1. sock_close 
  2.     |-sock_release 
  3.         |-sock->ops->release(sock); 

在上一篇博客中,我们知道sock->ops为下图所示:

即(在这里我们仅考虑tcp,即sk_prot=tcp_prot):

  1. inet_stream_ops->release 
  2.     |-inet_release 
  3.             |-sk->sk_prot->close(sk, timeout); 
  4.                 |-tcp_prot->close(sk, timeout); 
  5.                     |->tcp_prot.tcp_close 

关于fd与socket的关系如下图所示:

上图中红色线标注的是close(fd)的调用链

tcp_close

  1. void tcp_close(struct sock *sk, long timeout) 
  2.     if (sk->sk_state == TCP_LISTEN) { 
  3.         // 如果是listen状态,则直接设为close状态 
  4.         tcp_set_state(sk, TCP_CLOSE); 
  5.     } 
  6.     // 清空掉recv.buffer 
  7.     ...... 
  8.     // SOCK_LINGER选项的处理 
  9.     ...... 
  10.     else if (tcp_close_state(sk)){ 
  11.         // tcp_close_state会将sk从established状态变为fin_wait1 
  12.         // 发送fin包 
  13.         tcp_send_fin(sk); 
  14.     } 
  15.     ...... 

四次挥手

现在就是我们的四次挥手环节了,其中上半段的两次挥手下图所示:

首先,在tcp_close_state(sk)中已经将状态设置为fin_wait1,并调用tcp_send_fin

  1. void tcp_send_fin(struct sock *sk) 
  2.     ...... 
  3.     // 这边设置flags为ack和fin 
  4.     TCP_SKB_CB(skb)->flags = (TCPCB_FLAG_ACK | TCPCB_FLAG_FIN); 
  5.     ...... 
  6.     // 发送fin包,同时关闭nagle 
  7.     __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_OFF); 

如上图Step1所示。

接着,主动关闭的这一端等待对端的ACK,如果ACK回来了,就设置TCP状态为FIN_WAIT2,如上图Step2所示,具体代码如下:

  1. tcp_v4_do_rcv 
  2.     |-tcp_rcv_state_process 
  3. int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len) 
  4.     ...... 
  5.      
  6.     if (th->ack) { 
  7.         ... 
  8.         case TCP_FIN_WAIT1: 
  9.             // 这处判断是确认此ack是发送Fin包对应的那个ack 
  10.             if (tp->snd_una == tp->write_seq) { 
  11.                 // 设置为FIN_WAIT2状态 
  12.                 tcp_set_state(sk, TCP_FIN_WAIT2); 
  13.                 ...... 
  14.                 // 设定TCP_FIN_WAIT2定时器,将在tmo时间到期后将状态变迁为TIME_WAIT 
  15.                 // 不过是这时候改的已经是inet_timewait_sock了 
  16.                 tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); 
  17.                 ...... 
  18.             } 
  19.     } 
  20.      
  21.     switch(sk->sk_state) { 
  22.     case TCP_FIN_WAIT1: 
  23.     case TCP_FIN_WAIT2: 
  24.         ...... 
  25.     case TCP_ESTABLISHED: 
  26.         tcp_data_queue(sk, skb); 
  27.         queued = 1; 
  28.         break; 
  29.     } 
  30.     ..... 

值的注意的是,从TCP_FIN_WAIT1变迁到TCP_FIN_WAIT2之后,还调用tcp_time_wait设置一个TCP_FIN_WAIT2定时器,在tmo+(2MSL或者基于RTO计算超时)超时后会直接变迁到closed状态(不过此时已经是inet_timewait_sock了)。这个超时时间可以配置,如果是ipv4的话,则可以按照下列配置:

  1. net.ipv4.tcp_fin_timeout 
  2. /sbin/sysctl -w net.ipv4.tcp_fin_timeout=30 

如下图所示:

有这样一步的原因是防止对端由于种种原因始终没有发送fin,防止一直处于FIN_WAIT2状态。

接着在FIN_WAIT2状态等待对端的FIN,完成后面两次挥手:

由Step1和Step2将状态置为了FIN_WAIT_2,然后接收到对端发送的FIN之后,将会将状态设置为time_wait,如下代码所示:

  1. tcp_v4_do_rcv 
  2.     |-tcp_rcv_state_process 
  3.         |-tcp_data_queue 
  4.                 |-tcp_fin 
  5. static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th) 
  6.     switch (sk->sk_state) { 
  7.         ...... 
  8.         case TCP_FIN_WAIT1: 
  9.             // 这边是处理同时关闭的情况 
  10.             tcp_send_ack(sk); 
  11.             tcp_set_state(sk, TCP_CLOSING); 
  12.             break; 
  13.         case TCP_FIN_WAIT2: 
  14.              
  15.             // 收到FIN之后,发送ACK同时将状态进入TIME_WAIT 
  16.             tcp_send_ack(sk); 
  17.             tcp_time_wait(sk, TCP_TIME_WAIT, 0); 
  18.     } 

time_wait状态时,原socket会被destroy,然后新创建一个inet_timewait_sock,这样就能及时的将原socket使用的资源回收。而inet_timewait_sock被挂入一个bucket中,由

inet_twdr_twcal_tick定时从bucket中将超过(2MSL或者基于RTO计算的时间)的time_wait的实例删除。

我们来看下tcp_time_wait函数

  1. void tcp_time_wait(struct sock *sk, int state, int timeo) 
  2.     // 建立inet_timewait_sock 
  3.     tw = inet_twsk_alloc(sk, state); 
  4.     // 放到bucket的具体位置等待定时器删除 
  5.     inet_twsk_schedule(tw, &tcp_death_row, time,TCP_TIMEWAIT_LEN); 
  6.     // 设置sk状态为TCP_CLOSE,然后回收sk资源 
  7.     tcp_done(sk); 

具体的定时器操作函数为inet_twdr_twcal_tick,这边就不做描述了

被动关闭

close_wait

在tcp的socket时候,如果是established状态,接收到了对端的FIN,则是被动关闭状态,会进入close_wait状态,如下图Step1所示:

具体代码如下所示:

  1. tcp_rcv_state_process 
  2.     |-tcp_data_queue 
  3. static void tcp_data_queue(struct sock *sk, struct sk_buff *skb) 
  4.     ... 
  5.     if (th->fin) 
  6.         tcp_fin(skb, sk, th); 
  7.     ... 

我们再看下tcp_fin

  1. static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th) 
  2.     ...... 
  3.     // 这一句表明当前socket有ack需要发送 
  4.     inet_csk_schedule_ack(sk); 
  5.     ...... 
  6.     switch (sk->sk_state) { 
  7.             case TCP_SYN_RECV: 
  8.             case TCP_ESTABLISHED: 
  9.                  
  10.                 // 状态设置程close_wait状态 
  11.                 tcp_set_state(sk, TCP_CLOSE_WAIT); 
  12.                 // 这一句表明,当前fin可以延迟发送 
  13.                 // 即和后面的数据一起发送或者定时器到时后发送 
  14.                 inet_csk(sk)->icsk_ack.pingpong = 1; 
  15.                 break; 
  16.     } 
  17.     ...... 

这边有意思的点是,收到对端的fin之后并不会立即发送ack告知对端收到了,而是等有数据携带一块发送,或者等携带重传定时器到期后发送ack。

如果对端关闭了,应用端在read的时候得到的返回值是0,此时就应该手动调用close去关闭连接

  1. if(recv(sockfd, buf, MAXLINE,0) == 0){ 
  2.     close(sockfd) 

我们看下recv是怎么处理fin包,从而返回0的,上一篇博客可知,recv最后调用tcp_rcvmsg,由于比较复杂,我们分两段来看:

tcp_recvmsg第一段

  1. ...... 
  2.   // 从接收队列里面获取一个sk_buffer 
  3.   skb = skb_peek(&sk->sk_receive_queue); 
  4.   do { 
  5.       // 如果已经没有数据,直接跳出读取循环,返回0 
  6.       if (!skb) 
  7.           break; 
  8.       ...... 
  9.       // *seq表示已经读到多少seq 
  10.       // TCP_SKB_CB(skb)->seq表示当前sk_buffer的起始seq 
  11.       // offset即是在当前sk_buffer中已经读取的长度 
  12.       offset = *seq - TCP_SKB_CB(skb)->seq; 
  13.       // syn处理 
  14.       if (tcp_hdr(skb)->syn) 
  15.           offset--; 
  16.       // 此处判断表示,当前skb还有数据可读,跳转found_ok_skb 
  17.       if (offset < skb->len) 
  18.           goto found_ok_skb; 
  19.       // 处理fin包的情况 
  20.       // offset == skb->len,跳转到found_fin_ok然后跳出外面的大循环 
  21.       // 并返回0 
  22.       if (tcp_hdr(skb)->fin) 
  23.           goto found_fin_ok; 
  24.       BUG_TRAP(flags & MSG_PEEK); 
  25.       skb = skb->next
  26.   } while (skb != (struct sk_buff *)&sk->sk_receive_queue); 
  27.   ...... 

上面代码的处理过程如下图所示:

我们看下tcp_recmsg的第二段:

  1. found_ok_skb: 
  2.         // tcp已读seq更新 
  3.         *seq += used; 
  4.         // 这次读取的数量更新 
  5.         copied += used; 
  6.         // 如果还没有读到当前sk_buffer的尽头,则不检测fin标识 
  7.         if (used + offset < skb->len) 
  8.             continue
  9.         // 如果发现当前skb有fin标识,去found_fin_ok 
  10.         if (tcp_hdr(skb)->fin) 
  11.             goto found_fin_ok; 
  12.         ...... 
  13. found_fin_ok: 
  14.          
  15.         // tcp已读seq++ 
  16.         ++*seq; 
  17.         ... 
  18.         break; 
  19. } while(len > 0); 

由上面代码可知,一旦当前skb读完了而且携带有fin标识,则不管有没有读到用户期望的字节数量都会返回已读到的字节数。下一次再读取的时候则在刚才描述的tcp_rcvmsg上半段直接不读取任何数据再跳转到found_fin_ok并返回0。这样应用就能感知到对端已经关闭了。

如下图所示:

last_ack

应用层在发现对端关闭之后已经是close_wait状态,这时候再调用close的话,会将状态改为last_ack状态,并发送本端的fin,如下代码所示:

  1. void tcp_close(struct sock *sk, long timeout) 
  2.     ...... 
  3.     else if (tcp_close_state(sk)){ 
  4.         // tcp_close_state会将sk从close_wait状态变为last_ack 
  5.         // 发送fin包 
  6.         tcp_send_fin(sk); 
  7.     } 

在接收到主动关闭端的last_ack之后,则调用tcp_done(sk)设置sk为tcp_closed状态,并回收sk的资源,如下代码所示:

  1. tcp_v4_do_rcv 
  2.     |-tcp_rcv_state_process 
  3. int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len) 
  4.     ...... 
  5.      
  6.     if (th->ack) { 
  7.         ... 
  8.         case TCP_LAST_ACK: 
  9.             // 这处判断是确认此ack是发送Fin包对应的那个ack 
  10.             if (tp->snd_una == tp->write_seq) { 
  11.                     tcp_update_metrics(sk); 
  12.                     // 设置socket为closed,并回收socket的资源 
  13.                     tcp_done(sk); 
  14.                     goto discard; 
  15.             } 
  16.         ... 
  17.     } 

上述代码就是被动关闭端的后两次挥手了,如下图所示:

出现大量close_wait的情况

linux中出现大量close_wait的情况一般是应用在检测到对端fin时没有及时close当前连接。有一种可能如下图所示:

当出现这种情况,通常是minIdle之类参数的配置不对(如果连接池有定时收缩连接功能的话)。给连接池加上心跳也可以解决这种问题。

如果应用close的时间过晚,对端已经将连接给销毁。则应用发送给fin给对端,对端会由于找不到对应的连接而发送一个RST(Reset)报文。

操作系统何时回收close_wait

如果应用迟迟没有调用close_wait,那么操作系统有没有一个回收机制呢,答案是有的。

tcp本身有一个包活(keep alive)定时器,在(keep alive)定时器超时之后,会强行将此连接关闭。可以设置tcp keep alive的时间

  1. /etc/sysctl.conf 
  2. net.ipv4.tcp_keepalive_intvl = 75 
  3. net.ipv4.tcp_keepalive_probes = 9 
  4. net.ipv4.tcp_keepalive_time = 7200 

默认值如上面所示,设置的很大,7200s后超时,如果想快速回收close_wait可以设置小一点。但最终解决方案还是得从应用程序着手。

关于tcp keepalive包活定时器可见笔者另一篇博客:

https://my.oschina.net/alchemystar/blog/833981

进程关闭时清理socket资源

进程在退出时候(无论kill,kill -9 或是正常退出)都会关闭当前进程中所有的fd(文件描述符)

  1. do_exit 
  2.     |-exit_files 
  3.         |-__exit_files 
  4.             |-close_files 
  5.                     |-filp_close 

这样我们又回到了博客伊始的filp_close函数,对每一个是socket的fd发送send_fin

Java GC时清理socket资源

Java的socket最终关联到AbstractPlainSocketImpl,且其重写了object的finalize方法

  1. abstract class AbstractPlainSocketImpl extends SocketImpl 
  2.     ...... 
  3.      
  4.     protected void finalize() throws IOException { 
  5.         close() 
  6.     } 
  7.     ...... 

所以Java会在GC时刻会关闭没有被引用的socket,但是切记不要寄希望于Java的GC,因为GC时刻并不是以未引用的socket数量来判断的,所以有可能泄露了一堆socket,但仍旧没有触发GC。

总结

linux内核源代码博大精深,阅读其代码很费周折。之前读\<\>的时候由于有先辈引导和梳理,所以看书中所使用的BSD源码并不觉得十分费劲。直到现在自己带着问题独立看linux源码的时候,尽管有之前的基础,仍旧被其中的各种细节所迷惑。希望笔者这篇文章能帮助到阅读linux网络协议栈代码的人。

原文链接

https://my.oschina.net/alchemystar/blog/1821680

本文转载自微信公众号「解Bug之路」,可以通过以下二维码关注。转载本文请联系解Bug之路公众号。

 

来源:解Bug之路内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯