提问人:nh2 提问时间:10/25/2023 最后编辑:nh2 更新时间:11/4/2023 访问量:219
尽管文件描述符已关闭,但仍CLOSE_WAIT TCP 状态
CLOSE_WAIT TCP states despite closed file descriptors
问:
我的 Linux 服务器应用程序侦听端口并使用 正确关闭其所有文件描述符 (FD)。8000
close()
尽管如此,我有时会观察到多达 3000 个 TCP 连接:CLOSE_WAIT
# netstat -antp | grep CLOSE_WAIT
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 149 0 127.0.0.1:8000 127.0.0.1:49630 CLOSE_WAIT -
tcp 236 0 127.0.0.1:8000 127.0.0.1:48440 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:41748 CLOSE_WAIT -
tcp 149 0 127.0.0.1:8000 127.0.0.1:46064 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:56654 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:37502 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:56976 CLOSE_WAIT -
tcp 251 0 127.0.0.1:8000 127.0.0.1:36416 CLOSE_WAIT -
... ~3000 more of these ...
(netstat
正在运行,因此不会丢失数据。root
我知道当服务器应用程序没有连接到套接字的 FD 时会发生这种情况。这在 RFC793 的 TCP 状态图中进行了解释(例如更好的渲染,例如这里)以及例如 https://blog.cloudflare.com/this-is-strictly-a-violation-of-the-tcp-specification/CLOSE_WAIT
close()
但我知道我的服务器是正确的,因为在服务器上进程只显示 90 个打开的 FD,而不是 3000 个。close()
ls -1 "/proc/$(pidof myserver)/fd" | wc -l
正确关闭的进一步证据是,如上所示,没有列出与端口关联的程序(请参阅)。netstat -p
CLOSE_WAIT -
一些其他未解决的案例的集合,其中显示没有相关过程:CLOSE_WAIT -
- 如何删除CLOSE_WAIT套接字连接
- https://forum.huawei.com/enterprise/en/nfs-lock-services-are-affected-due-to-firewall-shielding-of-some-ports/thread/667243322445545472-667213878863474688
- https://serverfault.com/questions/311009/netstat-shows-a-listening-port-with-no-pid-but-lsof-does-not/847910#847910
- https://unix.stackexchange.com/questions/10106/orphaned-connections-in-close-wait-state
- 在这里,带有 as 进程的 s 看起来像是来自客户端的。
CLOSE_WAIT
-
- 在这里,带有 as 进程的 s 看起来像是来自客户端的。
所以问题来了:
怎么会存在比开放套接字 FD 更多的CLOSE_WAIT
状态?
为什么 Linux 在 /proc/$PID/fd
和 netstat
的输出上自相矛盾,CLOSE_WAIT
必须有一个未关闭的套接字 (FD) 与之关联,怎么可能发生CLOSE_WAIT
?
答:
在多种情况下,可以观察到这种情况,但如果没有关于服务器实现的细节,就不可能有效地回答这个问题。
首先,让我们澄清一下,你不能指望在 /proc/$PID/fd 中看到与你的进程无关的连接的文件句柄(这就是在显示输出的最后一列中没有你的进程 ID 所指示的),所以根本没有不一致,你可能只是对这些套接字在某个时间根据端口号与你的应用程序相关联的事实感到困惑使用过,但显然不是当前正在运行的进程,或者至少不是它,如下面的案例 1 示例所示:accept()
root@debian:~# ls -al /proc/1157/fd
total 0
dr-x------ 2 carenas carenas 0 Oct 29 22:39 .
dr-xr-xr-x 9 carenas carenas 0 Oct 29 22:39 ..
lrwx------ 1 carenas carenas 64 Oct 29 22:39 0 -> /dev/pts/0
lrwx------ 1 carenas carenas 64 Oct 29 22:39 1 -> /dev/pts/0
lrwx------ 1 carenas carenas 64 Oct 29 22:39 2 -> /dev/pts/0
lrwx------ 1 carenas carenas 64 Oct 29 22:39 3 -> 'anon_inode:[eventpoll]'
lrwx------ 1 carenas carenas 64 Oct 29 22:39 4 -> 'socket:[23882]'
root@debian:~# ls -al /proc/1199/fd
total 0
dr-x------ 2 carenas carenas 0 Oct 29 22:51 .
dr-xr-xr-x 9 carenas carenas 0 Oct 29 22:51 ..
lrwx------ 1 carenas carenas 64 Oct 29 22:51 0 -> /dev/pts/0
lrwx------ 1 carenas carenas 64 Oct 29 22:51 1 -> /dev/pts/0
lrwx------ 1 carenas carenas 64 Oct 29 22:51 2 -> /dev/pts/0
lrwx------ 1 carenas carenas 64 Oct 29 22:51 3 -> 'anon_inode:[eventpoll]'
lrwx------ 1 carenas carenas 64 Oct 29 22:51 4 -> 'socket:[23882]'
lrwx------ 1 carenas carenas 64 Oct 29 22:51 5 -> 'socket:[22177]'
root@debian:~# netstat -antpe | grep 7777
tcp 2 0 0.0.0.0:7777 0.0.0.0:* LISTEN 1000 23882 1157/./fork
tcp 0 0 127.0.0.1:60574 127.0.0.1:7777 ESTABLISHED 1000 21053 1198/perl
tcp 0 0 127.0.0.1:7777 127.0.0.1:60574 ESTABLISHED 1000 22177 1199/./fork
tcp 5 0 127.0.0.1:7777 127.0.0.1:59690 CLOSE_WAIT 0 0 -
tcp 5 0 127.0.0.1:7777 127.0.0.1:44830 CLOSE_WAIT 0 0 -
其次,您是对的,如果您的应用程序这样做,那么您不应该在CLOSE_WAIT中看到套接字,但您的应用程序可能没有这样做,因为:close()
- 这些连接在应用程序能够处理它们之前就被卡住了,因此在应用程序有机会这样做之前,客户端关闭了套接字。
- 由于涉及多个线程、进程甚至信号处理程序,FD 可能已被更改,因此除非检查 close() 的返回状态,否则它可能根本无效,如果您的应用程序依赖于操作系统关闭所有文件句柄,甚至可能不为 SIGCHLD 提供默认信号处理程序,这可能会导致进程成为僵尸。
exit()
案例 2 的一个有趣的示例(原始示例),但由于共享套接字,它们无论如何都是相关的,如下所示:
# netstat -antpe | grep 7777
tcp 3 0 0.0.0.0:7777 0.0.0.0:* LISTEN 1000 23882 1157/./fork
tcp 1 0 127.0.0.1:7777 127.0.0.1:37786 CLOSE_WAIT 0 0 -
tcp 1 0 127.0.0.1:7777 127.0.0.1:34022 CLOSE_WAIT 0 0 -
tcp 4 0 127.0.0.1:7777 127.0.0.1:60150 ESTABLISHED 0 0 -
tcp 0 0 127.0.0.1:60150 127.0.0.1:7777 ESTABLISHED 1000 21228 1406/perl
# netstat -antpe | grep 7777
tcp 0 0 0.0.0.0:7777 0.0.0.0:* LISTEN 1000 23882 1157/./fork
tcp 0 0 127.0.0.1:7777 127.0.0.1:60150 ESTABLISHED 1000 24277 1524/./fork
tcp 0 0 127.0.0.1:60150 127.0.0.1:7777 ESTABLISHED 1000 21228 1406/perl
在这里,客户端得到了一个无响应的服务器并被挂起,直到很久以后,当无响应的服务器最终被收割时,它连接到一个没有 pid 的套接字,直到最终将其连接“正式”移动到另一个服务器进程。
最后,工作 fd 与CLOSE_WAIT中的数量之间的极度不平衡意味着您的应用程序存在严重问题,接收队列中的高数字可能表明服务器变得无响应,因此这可能是客户端主动关闭连接并通过重试使问题变得更糟的 sideffect。
确保进程之间没有共享套接字,并且没有任何东西会阻塞侦听器套接字(例如:一些垃圾回收)可能会有所帮助。如果这只是一个容量问题,那么缩短侦听队列和水平扩展可能是最好的选择。
评论
close()
return state
CLOSE-WAIT
ss
nc
--connect=reset
我想通了:
当在 Linux 内核的 listen() 积压工作队列中等待的客户端在用户空间应用程序接受 ()
之前断开连接时,就会发生没有关联进程的CLOSE_WAIT
状态。
这很容易用 重现,见下文。netcat
关于这个问题的现有评论摘要:
- 我在上述评论中的直觉是正确的,如果没有显示任何关联的进程,它必须只涉及内核,而不涉及我的程序的文件描述符。
- 评论者的建议是不正确的,这些无进程的 s 是由服务器忘记调用 创建的。
CLOSE_WAIT
close()
- 评论者的建议是不正确的,内核的显示在某种程度上被窃听了。
/proc/<pid>/fd
复制nc
简短的再现摘要(阅读下面的解释):
nc -l 1234
nc localhost 1234
nc localhost 1234 # press Ctrl+C here
ss -tapn 'sport = :1234' # shows process-less `CLOSE-WAIT`
1 号航站楼(“服务器”):
nc -v -l 127.0.0.1 1234
这将创建一个套接字(并使用队列长度调用它,并调用以等待连接。listen(..., 1)
backlog
1
accept()
(可以用 验证。strace
旁白:
- 这个队列被称为“接受队列”(关于它的很深入的文章),包含要
接受()
的连接,但我在这里将其称为队列,因为它的大小由 决定,它的生存期由 .listen()
listen()
listen()
- Linux(在我的情况下)实际上将实际队列大小设置为 ,因此队列实际上有 2 个插槽。
我还没有研究过为什么会这样,但是在这里提到了它,并且我已经通过实验验证了它:
对于一个,3个客户端可以连接到上面的netcat服务器(1个版本,2个在队列中),只有第4个客户端在没有连接的情况下挂起。
6.1.51
backlog + 1
listen(, ...)
accept()
- 传递的大小可以作为以下输出中的字段进行观察。
backlog
Send-Q
ss -tlpn 'sport = :1234'
2 号航站楼(“客户 A”):
nc -v -4 127.0.0.1 1234
此连接在服务器上返回。accept()
3 号航站楼(“客户 B”):
nc -v -4 127.0.0.1 1234
此连接将填充服务器队列中的空位。它不是ed。listen()
accept()
现在,按 取消这个 .
这将从问题创建状态。Ctrl+C
nc
CLOSE_WAIT
观察输出ss
如果我们在另一个终端中观看时运行上述重现,我们可以观察上述每个步骤后的状态:sudo watch -n1 --exec ss -tapn 'sport = :1234'
# After the server is started, we see the listening socket with a `Recv-Q` of `0`:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 1 127.0.0.1:1234 0.0.0.0:* users:(("nc",pid=3613079,fd=3))
# After client A is started, we see the listening socket with a `Recv-Q` of `0`
# because client A was `accept()`ed:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 1 127.0.0.1:1234 0.0.0.0:* users:(("nc",pid=3613079,fd=3))
ESTAB 0 0 127.0.0.1:1234 127.0.0.1:52190 users:(("nc",pid=3613079,fd=4))
# After client B is started, we see the listening socket with a `Recv-Q` of `1`
# because client B has not yet been `accept()`ed and is in the queue:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 1 1 127.0.0.1:1234 0.0.0.0:* users:(("nc",pid=3613079,fd=3))
ESTAB 0 0 127.0.0.1:1234 127.0.0.1:42420
ESTAB 0 0 127.0.0.1:1234 127.0.0.1:52190 users:(("nc",pid=3613079,fd=4))
在最后一步中,我们已经可以观察到与空的联系。
这是因为连接确实是启用的 -- 但仅限于服务器的内核,而不是服务器进程,因为进程尚未建立连接。ESTAB
Process
accept()
内核为我们执行 TCP SYN-ACK-ACK 握手,因此在发生之前在内核中建立了 TCP 连接。accept()
现在,在客户端 B 断开连接后,我们看到 without 进程:CLOSE-WAIT
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 1 1 127.0.0.1:1234 0.0.0.0:* users:(("nc",pid=3621113,fd=3))
ESTAB 0 0 127.0.0.1:1234 127.0.0.1:52628 users:(("nc",pid=3621113,fd=4))
CLOSE-WAIT 1 0 127.0.0.1:1234 127.0.0.1:45096
并且仍然,所以断开连接仍然在内核队列中!Recv-Q
1
观察输出netstat
同:netstat
sudo watch -n1 'netstat -antpe | grep 1234'
我们看到更多的行,因为无法进行提供的方便的筛选,所以我们也看到了客户端套接字:netstat
'sport = :1234'
ss
# After the server is started:
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 0 0 127.0.0.1:1234 0.0.0.0:* LISTEN 1000 62603813 3621113/nc
# After client A is started:
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 0 0 127.0.0.1:1234 0.0.0.0:* LISTEN 1000 62603813 3621113/nc
tcp 0 0 127.0.0.1:52628 127.0.0.1:1234 ESTABLISHED 1000 62608860 3621978/nc
tcp 0 0 127.0.0.1:1234 127.0.0.1:52628 ESTABLISHED 1000 62603814 3621113/nc
# After client B is started:
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 1 0 127.0.0.1:1234 0.0.0.0:* LISTEN 1000 62603813 3621113/nc
tcp 0 0 127.0.0.1:52628 127.0.0.1:1234 ESTABLISHED 1000 62608860 3621978/nc
tcp 0 0 127.0.0.1:1234 127.0.0.1:52628 ESTABLISHED 1000 62603814 3621113/nc
tcp 0 0 127.0.0.1:45096 127.0.0.1:1234 ESTABLISHED 1000 62609455 3622106/nc
tcp 0 0 127.0.0.1:1234 127.0.0.1:45096 ESTABLISHED 0 0 -
同样,在这里我们首先寻求连接。-
PID/Program name
ESTABLISHED
这也回答了这个问题:
在客户端 B 断开连接后:
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 1 0 127.0.0.1:1234 0.0.0.0:* LISTEN 1000 62603813 3621113/nc
tcp 0 0 127.0.0.1:52628 127.0.0.1:1234 ESTABLISHED 1000 62608860 3621978/nc
tcp 0 0 127.0.0.1:1234 127.0.0.1:52628 ESTABLISHED 1000 62603814 3621113/nc
tcp 0 0 127.0.0.1:45096 127.0.0.1:1234 FIN_WAIT2 0 0 -
tcp 1 0 127.0.0.1:1234 127.0.0.1:45096 CLOSE_WAIT 0 0 -
这就是我们的流程。CLOSE_WAIT
-
使用 Python 进行更简约的重现
由于可能会随着时间的推移而改变它的确切功能(例如,系统调用它),因此在 Python 中有一个类似的 TCP 服务器,可以更加清楚地了解服务器上发生的事情:nc
#!/usr/bin/env python3
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: # TCP
s.bind(('127.0.0.1', 1234))
s.listen(1)
while True:
conn, addr = s.accept()
print(f"Got client {addr} as FD {conn.fileno()}")
action = input("Press enter to close the current connection and call accept() again, or enter 'close-socket' to close the entire socket... ").strip()
if action == "close-socket":
s.close()
input("Socket closed, press enter to terminate... ")
exit()
else:
conn.close()
为什么存在无过程以及如何摆脱它CLOSE_WAIT
内核将断开连接的连接保留在 定义的队列中,而没有关联的进程,直到:CLOSE_WAIT
listen()
- 服务器的套接字由 返回,或者
close()
listen()
- 服务器的连接已断开连接。
accept()
接受已经断开连接的连接将成功:将向服务器提供 FD。
这会将 without 进程转换为 with 进程。
服务器现在可以调用 FD 来关闭连接并解析状态。accept()
CLOSE_WAIT
CLOSE_WAIT
close()
CLOSE_WAIT
调用返回的套接字会破坏整个内核队列,从而立即消失。close()
listen()
CLOSE_WAIT
总结
- 无进程是进入队列并被发送 TCP 的另一方终止的连接,然后在我们的进程之前从队列中取出。
CLOSE_WAIT
listen()
FIN
listen()
accept()
- 它们生活在内核中。
- 它们没有与之关联的文件描述符 (FD),因为尚未为它们分配 FD。
accept()
- 这就解释了为什么 s 可以多于文件描述符。
CLOSE_WAIT
3000 个这样的无进程问题表明服务器没有连接。
这可能是由于错误,或者因为服务器进程正忙于做其他事情(例如垃圾回收或运行其他一些功能)。
因此,队列已满。
服务器必须已调用或更高。确实,我可以看到在.
当排队的客户端最终由于超时而放弃时,排队的连接将成为排队的连接。CLOSE_WAIT
accept()
listen(, 3000)
Send-Q
4096
ss -tlpn 'sport = :8000'
ESTABLISHED
CLOSE_WAIT
因此,解决多达 3000 个连接的问题的下一步应该是弄清楚服务器停止调用 的原因。CLOSE_WAIT
accept()
评论
CLOSE_WAIT
CLOSE_WAIT
TIME_WAIT