很多资料讲了关于tcp的closing和close_wait状态以及所谓的优雅关闭的细节,多数侧重与linux的内核实现(除了《unix网络编程》)。本文不注重代码细节,只关注逻辑。所使用的工具,tcpdump,packetdrill以及ss。
关于ss可以先多说几句,它展示的信息跟netstat差不多,只不过更加详细。netstat的信息是通过procfs获取的,本质上来讲就是遍历/proc/net/netstat文件的内容,然后将其组织成可读的形式展示出来,然而ss则可以针对特定的五元组信息提供更加详细的内容,它不再通过procfs,而是用过netlink来提取特定socket的信息,对于tcp而言,它可以提取到甚至tcp_info这种详细的信息,它包括cwnd,ssthresh,rtt,rto等。
本文展示的逻辑使用了以下三样工具:
1).packetdrill
使用packetdrill构造出一系列的包序列,使得tcp进入closing状态或者close_wait状态。
2).tcpdump/tshark
抓取packetdrill注入的数据包以及协议栈反馈的包,以确认数据包序列确实如tcp标准所述的那样。
3).ss/netstat
通过ss抓取packetdrill相关套接字的tcp_info,再次确认细节。
我想,我使用上述的三件套解析了closing状态之后,接下来的close_wait状态就可以当作练习了。
我来一个一个说。
1.关于closing状态首先我来描述一下而不是细说概念。
什么是closing状态呢?我们来看一下下面的局部状态图:
也就是说,当两端都主动发送fin的时候,并且在收到对方对自己发送的fin之前收到了对方发送的fin的时候,两边就都进入了closing状态,这个在状态图上显示的很清楚。这个用俗话说就是”同时关闭“。时序图我就不给出了,请自行搜索或者自己画。
有很多人都说,这种状态的tcp连接在系统中存在了好长时间并百思不得其解。这到底是为什么呢?通过状态图和时序图,我们知道,在进入closing状态后,只要收到了对方对自己的fin的ack,就可以双双进入time_wait状态,因此,如果rtt处在一个可接受的范围内,发出的fin会很快被ack从而进入到time_wait状态,closing状态应该持续的时间特别短。
以下是packetdrill脚本,很简单的一个脚本:
0.000 socket(..., sock_stream, ipproto_tcp) = 30.000 setsockopt(3, sol_socket, so_reuseaddr, [1], 4) = 00.000 bind(3, ..., ...) = 00.000 listen(3, 1) = 00.100 s. 0:0(0) ack 1 win 5840 0.200 < . 1:1(0) ack 1 win 2570.200 accept(3, ..., ...) = 4// 象征性写入一些数据,装的像一点一个正常的tcp连接:握手-传输-挥手0.250 write(4, ..., 1000) = 10000.300 < . 1:1(0) ack 1001 win 257// 主动断开,发送fin0.400 close(4) = 0// 在未对上述close的fin进行ack前,先fin0.500 sk_wmem_alloc) > min(sk- >sk_wmem_queued + (sk- >sk_wmem_queued > > 2), sk- >sk_sndbuf)) return -eagain;在linux协议栈的实现中,tcp_retransmit_skb由tcp_retransmit_timer调用,即便是这里出了些问题没有重传成功,也还是会退避的,退避超时到期后,继续在这里出错,直到”不可容忍“销毁socket。
我们可以得知,不管如何closing状态的tcp连接即便没有收到对自己fin的ack,也不会永久保持下去,保持多久取决于自己发送fin时刻的rtt,然后rtt计算出的rto按照最大的退避次数来退避,直到最终执行了固定次数的退避后,算出来的那个比较大的超时时间到期,然后tcp socket就销毁了。
因此,closing状态并不可怕,起码,不管怎样,它有一个可控的销毁时限。
...
现在我来解释重传不成功的细节。
我们知道,根据上述的代码段,sk_wmem_alloc要足够大,大到它比sk_wmem_queued+sk_wmem_queued/4更大的时候,才会返回错误造成重传不成功,然而我们的packetdrill脚本中构造的tcp连接的生命周期中仅仅传输了1000个字节的数据,并且这1000个字节的数据得到了ack,然后就结束了连接。一个socket保有一个sk_wmem_alloc字段,在skb交给这个socket的时候,该字段会增加skb长度的大小(skb本身大小包括skb数据大小),然而当skb不再由该socket持有的时候,也就是其被更底层的逻辑接管之后,socket的sk_wmem_alloc字段自然会减去skb长度的大小,这一切的过程由以下的函数决定,即skb_set_owner_w和skb_orphan。我们来看一下这两个函数:
static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk){ skb_orphan(skb); skb- >sk = sk; // sock_wfree回调中会递减sk_wmem_alloc相应的大小,其大小就是skb- >truesize skb- >destructor = sock_wfree; /* * we used to take a refcount on sk, but following operation * is enough to guarantee sk_free() wont free this sock until * all in-flight packets are completed */ atomic_add(skb- >truesize, &sk- >sk_wmem_alloc);}static inline void skb_orphan(struct sk_buff *skb){ // 调用回调函数,递减sk_wmem_alloc if (skb- >destructor) skb- >destructor(skb); skb- >destructor = null; skb- >sk = null;}也就是说,只要skb_orphan在skb通向网卡的路径上被正确调用,就会保证sk_wmem_alloc的值随着skb进入socket的管辖时而增加,而被实际发出后而减少。但是根据我的场景,事实好像不是这样,sk_wmem_alloc的值只要发送一个skb就会增加,丝毫没有减少的迹象...这是为什么呢?
有的时候,当你对某个逻辑理解足够深入后,一定要相信自己的判断,内核存在bug!内核并不完美。我使用的是2.6.32老内核,这个内核我已经使用了6年多,这是我在这个内核上发现的第4个bug了。
请注意,我的这个场景中,我使用了packetdrill来构造数据包,而packetdrill使用了tun网卡。为什么使用真实网卡甚至使用loopback网卡就不会有问题呢?这进一步引导我去调查tun的代码,果不其然,在其hard_xmit回调中没有调用skb_orphan!也就说说,但凡使用2.6.32内核版本tun驱动的,都会遇到这个问题呢。在tun的xmit中加入skb_orphan之后,问题消失,抓包你会发现大量的fin重传包,这些重传随着退避而间隔加大(注意,用ss命令比对一下rto字段的值和tcpdump抓取的实际值):
(为了验证这个,我修改了packetdrill脚本,中间增加了很多的数据传输,以便尽快重现sk_wmem_alloc在使用tun时不递减的问题)于是,我联系了前公司的同事,让他们修改openvpn使用的tun驱动代码,因为当时确实出现过关于tcp使用openvpn隧道的重传问题,然而,得到的答复却是,xmit函数中已经有skb_orphan了...然后我看了下代码,发现,公司的代码已经不存在问题了,因为我在前年搞tun多队列的时候,已经移植了3.9.6的tun驱动,这个问题已经被修复。
自己曾经做的事情,已然不再忆起...
2.关于close_wait状态和closing状态不同,close_wait状态可能会持续更久更久的时间,导致无用的socket无法释放,这个时间可能与应用进程的生命周期一样久!
我们先看一下close_wait的局部状态图。
然后我来构造一个packetdrill脚本:
0.000 socket(..., sock_stream, ipproto_tcp) = 30.000 setsockopt(3, sol_socket, so_reuseaddr, [1], 4) = 00.000 bind(3, ..., ...) = 00.000 listen(3, 1) = 00.100 s. 0:0(0) ack 1 win 14600 0.200 < . 1:1(0) ack 1 win 2570.200 accept(3, ..., ...) = 4// 什么也不发了,直接断开0.350 fd[fd]; ... retval = filp_close(filp, files); ... return retval; ...}export_symbol(sys_close);在filp_close中会有fput调用:
void fput(struct file *file){ if (atomic_long_dec_and_test(&file- >f_count)) __fput(file);}看到那个引用计数了吗?只有当这个文件的引用计数变成0的时候,才会调用底层的关闭逻辑,对于socket而言,如果仍然还有一个进程或者线程持有这个socket对应的文件系统的描述符,那么即便你调用了close,也不会进入了socket的close逻辑,它在文件系统层面就返回了!
shutdown调用
这个才是真正关闭一个tcp连接的调用!shutdown并没有文件系统的语义,它专门针对内核层的tcp socket。因此,调用shutdown的逻辑,才是真正关闭了与之共享信道的tcp socket。
所谓的优雅关闭,就是在调用close之前, 首先自己调用shutdown(rd or wd)。这样的时序才是关闭tcp的必由之路!
如果你想优雅关闭一个tcp连接,请先用shutdown,然后后面跟一个close。不过有点诡异的是,linux的shutdown(shut_rd)貌似没有任何效果,不过这无所谓了,本来对于读不读的,就不属于tcp的范畴,只有shut_wr才会实际发送一个fin给对方。
意法半导体推出业界首款同时适用于单电阻采样和三电阻采样的低电压无刷电机驱动器STSPIN233
智能家居生态带来了前所未有的挑战和机遇
动力电池回收及梯次利用,眼前满是荆棘,未来前景可期
科大讯飞布新款智能鼠标,内置语音识别,可一分钟语音输入400字
壹传诚VR一体机助力顺德一中禁毒宣传
什么是CLOSING状态
中兴通讯用最靠近客户的一线场景拉动数字治理
安全!安全!安全!极海大川GS500工业互联网SoC-eSE安全主控芯片
如何通过接地技术来抑制EMI干扰
索尼松下三洋垄断锂电池价格 遭欧盟罚款1.76亿美元
Arduino引脚监视器护罩的说明
AD转换设计中的基本问题整理
采用CPU通信功能同步AGV和车体传送带的控制,三大解决方案介绍
智能电网中的分布式发电技术
Vishay的新款表面贴装Power Metal Strip电阻可节省系统空间,提高系统效率
传统的蓄电池维护测试技术发展历程及效果分析(三)
维护全球化,促进国际合作
物联网的下一章怎样
小米6什么时候上市:金属机身+双版本+ MIUI9+陶瓷版,售价1999?
小米新Air笔记本电脑发布:18日开售4999元起