一、程序运行过程中,一方突然终止进程会怎么样
TCP连接建立以后,客户端和服务端都需要依靠操作系统维护socket。
我们之前知道,socket本质上也属于文件:
进程
↓
文件描述符表
↓
sockfd
↓
struct file
↓
struct socket
↓
struct sock
↓
TCP连接
因此,当程序结束时,TCP连接最终也会随着socket文件描述符的关闭而被处理。
1. 进程正常退出
例如客户端正在运行:
客户端 A 服务端 B
│ │
│──────────── TCP数据 ───────────────────────►│
│ │
此时客户端调用:
exit(0);
或者正常从 main() 返回。
进程退出时,操作系统会负责清理这个进程打开的文件描述符。
也就是:
进程退出
↓
关闭进程打开的文件描述符
↓
关闭socket
↓
TCP进行正常关闭
↓
发送FIN
所以:
程序退出并不会直接导致TCP连接凭空消失,本质上还是操作系统在清理进程资源时关闭了socket。
最终TCP还是会按照正常的连接关闭流程处理。
2. 进程被强制终止呢?
这里需要区分一个非常容易混淆的地方。
例如:
kill -9 PID
这属于强制终止进程。
虽然程序本身没有机会执行:
close(sockfd);
但是:
kill -9****杀掉的是用户态进程,并不代表操作系统内核也没了。
进程被终止以后,内核依然会负责清理这个进程的资源,其中就包括文件描述符。
所以过程仍然是:
kill -9 PID
↓
进程被内核终止
↓
内核清理进程资源
↓
关闭文件描述符
↓
关闭socket
↓
TCP进行正常关闭
↓
发送FIN
因此,从TCP连接的角度来看,进程被强制终止和正常退出通常没有本质区别。
区别主要在于:
正常退出:
程序自己执行退出流程
↓
正常结束
kill -9:
程序来不及自己清理
↓
内核强制终止并清理资源
但是最终操作系统仍然会负责释放这个进程持有的socket资源。
所以可以记住:
进程终止 ≠ 主机断电。只要操作系统内核还活着,进程退出以后,内核仍然可以清理socket。
3. 为什么进程退出以后TCP连接会关闭
因为对于Linux来说:
socket
本身也是一种文件。
程序中的:
int sockfd;
只是文件描述符表中的一个下标。
所以:
sockfd
↓
struct file
↓
socket
当进程退出时,操作系统会清理这个进程的文件描述符。
因此最终就是:
进程退出
↓
文件描述符关闭
↓
socket关闭
↓
TCP连接关闭
这里要注意:
不是因为"进程没了,所以TCP立刻没了",而是因为进程退出时,操作系统会清理它打开的socket文件描述符。
4. 如果只是关闭某个socket呢?
其实不需要等整个进程退出。
例如:
close(sockfd);
也会关闭这个socket对应的文件描述符。
过程就是:
close(sockfd)
↓
关闭socket文件描述符
↓
TCP开始关闭连接
所以:
close(sockfd)
和:
进程退出
最终都可能导致socket被关闭。
区别只是:
close()
→ 主动关闭某个socket
进程退出
→ 操作系统统一清理进程拥有的文件描述符
二、机器重启会怎么样?
机器重启需要单独看。
如果是正常重启,例如系统执行:
reboot
那么操作系统在重启过程中仍然有机会进行资源清理。
从TCP连接的角度来看,它和进程终止的思路比较接近:
正常重启
↓
操作系统仍然运行
↓
关闭相关资源
↓
关闭socket
↓
TCP正常关闭
所以:
正常重启和进程终止类似,本质上都是操作系统还活着,可以进行socket资源清理。
但是如果不是正常重启,而是直接:
突然断电
那就完全不同了。
因为:
正常重启:
操作系统还活着
↓
可以清理socket
↓
可以进行TCP关闭流程
而:
突然掉电:
电源消失
↓
CPU停止运行
↓
操作系统停止运行
↓
没有机会清理socket
↓
没有机会发送FIN
所以这里真正需要区分的是:
进程终止 / 正常重启
VS
突然掉电
而不是简单地认为"机器重启一定和掉电一样"。
三、一方突然关机或者网线断了怎么办
1. 突然关机和进程退出完全不同
假设客户端A正常退出:
A
↓
进程退出
↓
操作系统还在
↓
关闭socket
↓
TCP发送FIN
但是如果客户端直接断电:
A
X
突然断电
这时候操作系统都没了。
因此A根本没有机会执行:
close(sockfd)
也没有机会发送:
FIN
于是服务端B可能还认为:
TCP连接 = ESTABLISHED
2. 网线突然断开也是类似的
例如:
客户端 A 服务端 B
│ │
│────── TCP连接 ───────────│
│ │
X
网线断开
网络突然断开以后,A和B都不会自动收到一个:
"网络已经断开"
的TCP报文。
所以TCP连接可能不会马上消失。
例如:
网络断开
↓
双方暂时没有数据
↓
TCP没有新的通信
↓
连接可能暂时仍然保持ESTABLISHED
这也是为什么:
TCP处于ESTABLISHED状态,并不代表对方主机此刻一定还在线。
四、掉电或者断网以后,TCP怎么发现对方已经不在了?
1. 接收端主动写数据
假设A突然掉电了,但是B暂时不知道。
此时B可能仍然认为:
TCP连接 = ESTABLISHED
如果B随后向A发送数据:
发送端 B 接收端 A
│ │
│──────────── TCP数据 ───────────────────────►│
│ X
但是A已经掉电,数据自然无法得到正常处理。
于是B可能经历:
发送数据
↓
等待ACK
↓
没有收到ACK
↓
超时
↓
重新发送
↓
仍然没有响应
↓
最终判断连接异常
↓
释放连接
所以:
即使对方已经掉电,TCP连接也不会凭空瞬间消失,而是需要通过后续通信逐渐发现对方已经不可达。
2. TCP本身还有保活机制
如果双方长时间没有任何数据通信,上面的"发送数据以后发现没有ACK"可能一直不会发生。
因此TCP还提供了:
TCP Keepalive
也就是TCP保活机制。
它的思路非常简单:
长时间没有通信
↓
启动保活检测
↓
定期询问对方
↓
对方正常响应
↓
说明连接仍然存在
如果对方已经掉电:
长时间没有通信
↓
启动保活检测
↓
发送探测
↓
对方没有响应
↓
继续探测
↓
仍然没有响应
↓
判断连接已经失效
↓
释放TCP连接
因此,即使应用层一直没有发送数据,TCP也可以通过保活机制检测长时间没有响应的连接。
需要注意的是,TCP保活并不是"断网瞬间就能发现",而是经过一定时间的探测和等待以后,才会判断连接失效。
五、应用层也可以自己检测连接是否还活着
除了TCP自己的机制之外,应用层协议也经常会设计自己的连接检测机制。
例如一个长时间保持的TCP连接:
客户端 A 服务端 B
│ │
│──────────── 心跳/探测 ──────────────────────►│
│ │
│◄─────────── 心跳响应 ────────────────────────│
│ │
如果客户端连续发送心跳:
心跳
↓
没有响应
↓
再次发送
↓
还是没有响应
↓
认为对方已经失联
那么应用程序就可以主动关闭本地连接,并进行重新连接。
例如:
TCP连接
↓
应用层定期发送心跳
↓
对方正常响应
↓
继续保持连接
如果发现:
连续多个心跳都没有响应
应用层就可以认为:
对方可能已经掉线
然后:
关闭旧连接
↓
重新建立TCP连接
这也是很多长连接程序常见的做法。
例如即时通信、长连接服务等,都可能在应用层设计自己的心跳和重连机制。
六、把几种情况放在一起
现在可以把TCP连接异常分成几种情况来看:
TCP连接
│
┌─────────────┴─────────────┐
│ │
▼ ▼
操作系统还活着 操作系统已经消失
│ │
┌───────┴────────┐ ┌─────┴─────┐
│ │ │ │
▼ ▼ ▼ ▼
进程终止 正常重启 突然掉电 网线断开
│ │ │ │
└───────┬────────┘ │ │
▼ │ │
内核清理socket │ │
│ │ │
▼ │ │
发送FIN │ │
│ │ │
▼ │ │
正常关闭 │ │
▼ ▼
无法发送FIN 无法发送FIN
│ │
└─────┬─────┘
▼
对端暂时不知道
│
┌───────────┴───────────┐
│ │
▼ ▼
后续发送数据 TCP保活
│ │
▼ ▼
没有ACK响应 探测无响应
│ │
└───────────┬───────────┘
▼
判断连接失效
│
▼
释放连接
七、最终记住这几个区别
1. 进程正常退出
进程退出
↓
内核清理文件描述符
↓
关闭socket
↓
TCP正常关闭
↓
发送FIN
2. 进程被强制终止
例如:
kill -9 PID
虽然程序本身来不及调用:
close(sockfd);
但是:
进程被终止
↓
内核仍然存在
↓
内核清理文件描述符
↓
关闭socket
↓
TCP正常关闭
所以它和正常关闭在TCP连接释放这一点上通常没有本质区别。
3. 正常机器重启
正常重启
↓
操作系统仍然运行
↓
清理相关资源
↓
关闭socket
↓
TCP进行正常关闭
4. 突然掉电
突然掉电
↓
操作系统停止运行
↓
没有机会关闭socket
↓
没有FIN
↓
对端暂时不知道
↓
后续通信/保活探测
↓
发现没有响应
↓
判断连接失效
5. 网线突然断开
网线断开
↓
无法正常通信
↓
不会立刻收到"连接断开"报文
↓
连接可能暂时仍然ESTABLISHED
↓
后续数据没有ACK
或
TCP保活没有响应
↓
最终判断连接失效
因此最核心的一句话就是:
只要操作系统内核还活着,即使进程被强制终止,内核也可以清理它的文件描述符并关闭socket;而机器突然掉电或者网络突然断开时,内核没有机会发送FIN,对端只能通过后续通信、TCP保活等机制逐渐发现连接已经失效。