Day: 2018年1月12日

TCP连接的关闭

TCP连接的关闭

多线程多进程关闭连接的区别

  首先来看看close和shutdown两个系统调用对应的内核函数:

TCP连接的关闭

 

TCP连接的关闭

  上图是调用close和shutdown关闭连接时的函数调用过程,sys_close和sys_shutdown这两个系统调用最终是由tcp_close和tcp_shutdown方法来实现的。

  由此我们可以来看一个问题:当socket被多进程或者多线程共享时,关闭连接时有何区别? 
  从图中可以看到sys_close封装调用过程中有一个fput函数,它有一个引用计数,记录这个socket被引用了多少次。当这个引用计数不为0时,是不会触发真正关闭tcp连接的tcp_close方法的。

  引用计数是怎么来的呢?创建进程和线程都是由clone系统调用实现的,只不过clone的参数不同,行为也不同。

  在clone系统调用中,会调用方法copy_files来拷贝文件描述符(包括socket)。创建线程时,传入的flag参数中包含标志位CLONE_FILES,此时,线程将会共享父进程中的文件描述符。

  而创建进程时没有这个标志位,这时,会把进程打开的所有文件描述符的引用计数加1。所以子进程会将父进程中已经建立的socket加上引用计数,当进程中close一个socket时,只会减少引用计数,仅当引用计数为0时才会触发tcp_close。

  单线程(进程)中使用close与多线程中是一致的,但这两者与多进程的行为并不一致,多进程中共享的同一个socket必须都调用了close才会真正的关闭连接。

  shutdown是没有引用计数什么事的,只要调用了就会去试图按需关闭连接。所以shutdown与多线程、多进程无关。

close发生了什么

  TCP连接是全双工的连接,及连接双方可以并行的发送或者接收消息。这样关闭连接时就存在3种情形:完全关闭连接; 关闭发送消息的功能;关闭接收消息的功能。其中,后者就叫做半关闭,由shutdown实现,前者用close实现。

  我们现在来看看close。

 

TCP连接的关闭

我们依次来看一下这三种情形:

1)关闭监听句柄

  用于listen的监听句柄是close关闭的,它本身并不对应着某个连接,但是附着在它之上的却可能有半成品连接,即1、2次握手完成,第3次未完成,就会在服务器上有许多半连接,close的工作主要是清理这些半连接。

  keepalive定时器是干嘛的?keepalive是TCP中一个可以检测死连接的机制,侧重在保持客户端和服务端的连接,防止僵死、异常退出的客户端占用服务器连接资源。

  比如A与B建立连接后,突然B宕机了,之后时间内B再也没有起来,如果B宕机后A和B一直没有数据通信的需求,A就永远不会发现B已经挂了,那么A的内核里还维护着一份关于A与B之间TCP连接的信息,浪费系统资源。

  所以TCP层面引入了keepalive机制,A会定期给B发送空的数据包,通俗讲就是心跳包。

  内核对于tcp keepalive的调整主要有以下三个参数:

TCP连接的关闭

  当tcp发现未收到对端数据时,开始以间隔tcp_keepalive_intvl(75)秒的频率发送心跳包。

  如果连续tcp_keepalive_probes(9)次以上未响应代表对端已经down了,close该连接。

  参照上图,close首先会移除keepalive定时器,移除此定时器后,若ESTABLISH状态的TCP连接在tcp_keepalive_time时间(如服务器上常配置为2小时)内没有通讯,服务器就会主动关闭连接。接下来,发送RST包去关闭每一个半连接,处理完所有半打开的连接close的任务就基本完成了。

2)关闭普通ESTABLISH状态的连接(未设置so_linger,默认)

  首先检查是否有接收到却未处理的消息。如果有,根据close的行为是要丢掉这些消息的。

  但丢弃消息后,意味着远端误以为发出的消息已经被本机处理了(因为ACK包确认过了),但实际上是收到未处理,此时不能使用正常的四次握手关闭连接,而是会向远端发送一个RST非正常复位关闭连接。所以这要求程序员在关闭连接前,确保接收并处理了连接上的消息。

  如果此时没有未处理的消息,那么发送FIN来关闭连接。这时,再看看是否有待发送消息,如果有,那么会在最后一个报文中加入FIN标志,同时关闭angle算法,一次性将所有包发出;如果没有未发送消息,则仅发送一个FIN报文,发送出去关闭连接。

3)使用了so_linger的连接

  so_linger是干嘛的?so_linger决定系统如何处理残留在套接字发送队列中的数据。我们可能有强可靠性的需求,也就是说,必须确保发出的消息、FIN都被对方收到。

  怎么做到呢?就是等待,close会阻塞住进程,直到对方确认收到了再返回,但是如果对方总是不响应怎么办呢?所以还需要l_linger这个超时时间,控制close阻塞进程的最长时间。

TCP连接的关闭

  上图是l_linger参数的设置及其对应的行为,默认情况下是第一种情况。

  当使用了so_linger后,前半段仍然没有变化。首先检查是否有未处理消息,有则发RST关连接。

  接下来检查是否有未发送消息,这种与第二种情况一致,设好FIN后关闭angle算法。接下来,会设置最大等待时间l_linger,然后进程开始睡眠,直到确认对方接收到消息,将控制权交还给用户进程。

  总结一下就是:调用close时,可能导致发送RST复位关闭连接,例如有未读消息、打开so_linger但l_linger却为0、关闭监听句柄时半打开的连接。更多时会导致发FIN来四次握手关闭连接,但打开so_linger可能导致close阻塞等待着对方的ACK表明收到了消息。

本文转自:小组15级成员--杜肖孟

原文地址:http://blog.csdn.net/tanswer_/article/details/78422879