基于linux 2.6.24内核版本浅谈socket的close

笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件exciting的事情。上篇博客讲了socket的阻塞和非阻塞,这篇就开始谈一谈socket的close(以tcp为例且基于linux-2.6.24内核版本)
tcp关闭状态转移图
众所周知,tcp的close过程是四次挥手,状态机的变迁也逃不出tcp状态转移图,如下图所示:
tcp的关闭主要分主动关闭、被动关闭以及同时关闭(特殊情况,不做描述)
主动关闭
close(fd)的过程
以c语言为例,在我们关闭socket的时候,会使用close(fd)函数:
intsocket_fd;
socket_fd = socket(af_inet,sock_stream,0);
...
// 此处通过文件描述符关闭对应的socket
close(socket_fd)
而close(int fd)又是通过系统调用sys_close来执行的:
asmlinkage longsys_close(unsignedintfd)
{
// 清除(close_on_exec即退出进程时)的位图标记
fd_clr(fd,fdt->close_on_exec);
// 释放文件描述符
// 将fdt->open_fds即打开的fd位图中对应的位清除
// 再将fd挂入下一个可使用的fd以便复用
__put_unused_fd(files,fd);
// 调用file_pointer的close方法真正清除
retval = filp_close(filp,files);
}
我们看到最终是调用的filp_close方法:
紧接着我们进入fput:
同一个file(socket)有多个引用的情况很常见,例如下面的例子:
所以在多进程的socket服务器编写过程中,父进程也需要close(fd)一次,以免socket无法最终关闭
然后就是_fput函数了:
由于我们讨论的是socket的close,所以,我们现在探查下file->f_op->release在socket情况下的实现:
f_op->release的赋值
我们跟踪创建socket的代码,即
socket_file_ops的实现为:
staticconststructfile_operations socket_file_ops = {
.owner = this_module,
......
// 我们在这里只考虑sock_close
.release = sock_close,
......
};
继续跟踪:
在上一篇博客中,我们知道sock->ops为下图所示:
即(在这里我们仅考虑tcp,即sk_prot=tcp_prot):
关于fd与socket的关系如下图所示:
上图中红色线标注的是close(fd)的调用链
tcp_close
四次挥手
现在就是我们的四次挥手环节了,其中上半段的两次挥手下图所示:
首先,在tcp_close_state(sk)中已经将状态设置为fin_wait1,并调用tcp_send_fin
voidtcp_send_fin(structsock *sk)
{
......
// 这边设置flags为ack和fin
tcp_skb_cb(skb)->flags = (tcpcb_flag_ack | tcpcb_flag_fin);
......
// 发送fin包,同时关闭nagle
__tcp_push_pending_frames(sk,mss_now,tcp_nagle_off);
}
如上图step1所示。 接着,主动关闭的这一端等待对端的ack,如果ack回来了,就设置tcp状态为fin_wait2,如上图step2所示,具体代码如下:
值的注意的是,从tcp_fin_wait1变迁到tcp_fin_wait2之后,还调用tcp_time_wait设置一个tcp_fin_wait2定时器,在tmo+(2msl或者基于rto计算超时)超时后会直接变迁到closed状态(不过此时已经是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,如下代码所示:
time_wait状态时,原socket会被destroy,然后新创建一个inet_timewait_sock,这样就能及时的将原socket使用的资源回收。而inet_timewait_sock被挂入一个bucket中,由 inet_twdr_twcal_tick定时从bucket中将超过(2msl或者基于rto计算的时间)的time_wait的实例删除。 我们来看下tcp_time_wait函数
voidtcp_time_wait(structsock *sk,intstate,inttimeo)
{
// 建立inet_timewait_sock
tw = inet_twsk_alloc(sk,state);
// 放到bucket的具体位置等待定时器删除
inet_twsk_schedule(tw, &tcp_death_row,time,tcp_timewait_len);
// 设置sk状态为tcp_close,然后回收sk资源
tcp_done(sk);
}
具体的定时器操作函数为inet_twdr_twcal_tick,这边就不做描述了
被动关闭
close_wait
在tcp的socket时候,如果是established状态,接收到了对端的fin,则是被动关闭状态,会进入close_wait状态,如下图step1所示:
具体代码如下所示:
我们再看下tcp_fin
这边有意思的点是,收到对端的fin之后并不会立即发送ack告知对端收到了,而是等有数据携带一块发送,或者等携带重传定时器到期后发送ack。
如果对端关闭了,应用端在read的时候得到的返回值是0,此时就应该手动调用close去关闭连接
if(recv(sockfd,buf,maxline,0) == 0){
close(sockfd)
}
我们看下recv是怎么处理fin包,从而返回0的,上一篇博客可知,recv最后调用tcp_rcvmsg,由于比较复杂,我们分两段来看:
tcp_recvmsg第一段
上面代码的处理过程如下图所示:
我们看下tcp_recmsg的第二段:
由上面代码可知,一旦当前skb读完了而且携带有fin标识,则不管有没有读到用户期望的字节数量都会返回已读到的字节数。下一次再读取的时候则在刚才描述的tcp_rcvmsg上半段直接不读取任何数据再跳转到found_fin_ok并返回0。这样应用就能感知到对端已经关闭了。 如下图所示:
last_ack
应用层在发现对端关闭之后已经是close_wait状态,这时候再调用close的话,会将状态改为last_ack状态,并发送本端的fin,如下代码所示:
在接收到主动关闭端的last_ack之后,则调用tcp_done(sk)设置sk为tcp_closed状态,并回收sk的资源,如下代码所示:
上述代码就是被动关闭端的后两次挥手了,如下图所示:
出现大量close_wait的情况
linux中出现大量close_wait的情况一般是应用在检测到对端fin时没有及时close当前连接。有一种可能如下图所示:
当出现这种情况,通常是minidle之类参数的配置不对(如果连接池有定时收缩连接功能的话)。给连接池加上心跳也可以解决这种问题。
如果应用close的时间过晚,对端已经将连接给销毁。则应用发送给fin给对端,对端会由于找不到对应的连接而发送一个rst(reset)报文。
操作系统何时回收close_wait
如果应用迟迟没有调用close_wait,那么操作系统有没有一个回收机制呢,答案是有的。 tcp本身有一个包活(keep alive)定时器,在(keep alive)定时器超时之后,会强行将此连接关闭。可以设置tcp keep alive的时间
/etc/sysctl.conf
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
默认值如上面所示,设置的很大,7200s后超时,如果想快速回收close_wait可以设置小一点。但最终解决方案还是得从应用程序着手。
关于tcp keepalive包活定时器可见笔者另一篇博客:
https://my.oschina.net/alchemystar/blog/833981
进程关闭时清理socket资源
进程在退出时候(无论kill,kill -9 或是正常退出)都会关闭当前进程中所有的fd(文件描述符)
这样我们又回到了博客伊始的filp_close函数,对每一个是socket的fd发送send_fin
java gc时清理socket资源
java的socket最终关联到abstractplainsocketimpl,且其重写了object的finalize方法
所以java会在gc时刻会关闭没有被引用的socket,但是切记不要寄希望于java的gc,因为gc时刻并不是以未引用的socket数量来判断的,所以有可能泄露了一堆socket,但仍旧没有触发gc。
总结
linux内核源代码博大精深,阅读其代码很费周折。之前读《tcp/ip详解卷二》的时候由于有先辈引导和梳理,所以看书中所使用的bsd源码并不觉得十分费劲。直到现在自己带着问题独立看linux源码的时候,尽管有之前的基础,仍旧被其中的各种细节所迷惑。希望笔者这篇文章能帮助到阅读linux网络协议栈代码的人。

CXL将成为跨计算引擎的内存结构标准
木林森披露2018年年度报告,2018年营收与净利均实现同比增长
“以人为本”明确智能家居系统总体规划
谷歌AlphaGO挑战赛——人工智能的边界在人类
桑顿260wh/kg电芯技术怎么样?
基于linux 2.6.24内核版本浅谈socket的close
陶瓷电容与安规Y电容的区别
未来VR设备也将能做得像眼镜一样纤薄
HT for Web 自主研发强大的基于 HTML5 的 2D、3D 渲染引擎
Gearbest.com将以150美元的价格提供联想K3 Note LTE智能手机
Visual Studio Code的安装和使用
孙学良JACS:界面键合卤化物实现快离子导电卤化物框架
合肥ECTA连接器 线束连接器 推拉式连接器
华大北斗成功入选“2021投资界硬科技VENTURE50”
Insta 360 发布了一款售价将近 10 万元的 VR 摄影机,可录制 11K 视频
中芯国际豪掷77亿购买光刻机
怎么样才能将旧电脑的重要软件移动到新电脑上?
智能家居品牌如何做好抖音?深层逻辑梳理,花小钱办大事
外媒:若继续抵制华为 欧美供应链将感受到痛苦
机器人艺术家“艾达”进入最后开发阶段 能模仿人类完成素描作品