多线程多进程关闭连接的区别
首先来看看close和shutdown两个系统调用对应的内核函数:
#define __NR_close 3
__SYSCALL(__NR_close, sys_close)
#define __NR_shutdown 48
__SYSCALL(__NR_shutdown, sys_shutdown)
上图是调用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
。
我们依次来看一下这三种情形:
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_keepalive_time:最后一次数据交换到TCP发送第一个保活探测报文的时间,即允许连接空闲的时间,默认两小时
tcp_keepalive_intvl:心跳包的重传时间,默认75s
tcp_keepalive_probes:心跳包的发送次数,默认9次
当tcp发现有tcp_keepalive_time(7200)
秒未收到对端数据时,开始以间隔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
阻塞进程的最长时间。
上图是l_linger参数的设置及其对应的行为,默认情况下是第一种情况。
当使用了so_linger后,前半段仍然没有变化。首先检查是否有未处理消息,有则发RST关连接。 接下来检查是否有未发送消息,这种与第二种情况一致,设好FIN后关闭nagle算法发出。接下来,会设置最大等待时间l_linger,然后进程开始睡眠,直到确认对方接收到消息,将控制权交还给用户进程。
总结一下就是:调用close时,可能导致发送RST复位关闭连接,例如有未读消息、打开so_linger但l_linger却为0、关闭监听句柄时半打开的连接。更多时会导致发FIN来四次握手关闭连接,但打开so_linger可能导致close阻塞住等待着对方的ACK表明收到了消息。
我没有用过 shutdown
,所以也没有仔细看,下面给出参考链接,上面的内容是参考原作者的文章,感谢。
参考链接:高性能网络编程–TCP连接的关闭