linux CLOSE_WAIT TCP状态,尽管文件描述符已关闭

w8ntj3qf  于 11个月前  发布在  Linux
关注(0)|答案(2)|浏览(461)

我的Linux服务器应用程序侦听端口8000,并使用close()正确关闭其所有文件描述符(FD)。
尽管如此,我有时会观察到多达3000个CLOSE_WAIT TCP连接:

# 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运行,因此没有丢失数据。
我知道CLOSE_WAIT会发生在服务器应用程序没有close()连接到套接字的FD时。这在RFC793的TCP状态图中有解释(更好的渲染,例如here),也在例如https://blog.cloudflare.com/this-is-strictly-a-violation-of-the-tcp-specification/中有解释。
但是我知道我的服务器正确地执行了close(),因为服务器进程上的ls -1 "/proc/$(pidof myserver)/fd" | wc -l只显示了90个打开的FD,而不是3000个。
正确关闭的进一步证据是上面显示的netstat -p没有列出与端口相关的程序(参见CLOSE_WAIT -)。
一些其他未解决的案例的集合,其中CLOSE_WAIT -显示没有相关的过程:

所以问题是:

CLOSE_WAIT状态怎么会比开放套接字FD多这么多?
为什么Linux在x1m15 n1x和x1m16 n1x的输出上自相矛盾,如果x1m18 n1x必须有一个未关闭的套接字(FD)与之关联,那么x1m17 n1x怎么可能发生呢?

2eafrhcq

2eafrhcq1#

在许多情况下都可以观察到这一点,但如果没有服务器实现的细节,就不可能有效地回答这个问题。
首先,让我们澄清一下,您不能期望在/proc/$PID/fd中看到与您的进程没有关联的连接的文件句柄(在输出的最后一列中没有您的进程ID表示这一点),因此根本不存在不一致性,并且您可能会对这些套接字在某个时候根据所使用的端口号与应用程序相关联这一事实感到困惑,但显然不是当前运行的进程,或者至少不是accept(),如下面案例1的示例所示:

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(),则在CLOSE_WAIT中不应看到套接字,但您的应用程序可能没有这样做,这是正确的,因为:
1.这些连接在您的应用程序能够处理它们之前就被卡住了,因此在您的应用程序有机会这样做之前,套接字就被客户端关闭了。
1.由于涉及到多个线程、进程甚至可能是信号处理程序,fd可能已经被改变,因此除非检查来自close()的返回状态,否则它可能根本就不有效,如果您的应用程序依赖于操作系统关闭exit()处的所有文件句柄,甚至可能不为SIGCHLD提供默认信号处理程序,这可能导致进程被保留为丧尸
第二种情况的一个有趣的例子(原始的),但是它们是相关的,因为共享的套接字如下所示:

# 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中fd的数量之间的极端不平衡意味着应用程序中存在严重问题,接收队列中的高数值可能表明服务器没有响应,因此这可能是客户端集体关闭连接的副作用,并通过重试使问题变得更糟。
确保进程之间没有共享套接字,并且没有任何东西会阻塞侦听器套接字(例如:一些垃圾收集),这可能会有所帮助。如果这只是一个容量问题,那么缩短侦听队列并水平扩展可能是最好的选择。

e0bqpujr

e0bqpujr2#

我想明白了:

当在Linux内核的listen() backlog队列中等待的客户端在用户空间应用程序accept()打开它之前断开连接时,会出现无关联进程的CLOSE_WAIT状态。

这很容易用netcat重现,见下文。
关于这一问题的现有评论摘要:

  • 我在上面的评论中的直觉是正确的,如果没有显示相关的进程,它必须只涉及内核,而不是我的程序的文件描述符。
  • 评论者的建议是不正确的,这些无进程的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,并调用accept()以等待连接。
(Can使用strace进行验证。)
旁白:

  • 这个队列被称为“接受队列”(great in-depth article about it),包含 * 将被accept() ed* 的连接,但我在这里将其称为listen()队列,因为它的大小由listen()决定,它的生存时间由listen()返回的套接字决定。
  • Linux(在我的例子中是6.1.51)实际上将真实的队列大小设置为backlog + 1,所以队列实际上有2个插槽。我还没有研究为什么会这样,但它在这里被提到,我已经通过实验验证了它:对于listen(, ...),3个客户端可以连接到上面的netcat服务器(1个accept()艾德,2个在队列中),只有第4个客户端在没有连接的情况下会挂起。
  • 传递的backlog大小可以在下面的ss -tlpn 'sport = :1234'输出中观察到Send-Q字段。

终端2(“客户端A”):

nc -v -4 127.0.0.1 1234


此连接使accept()返回服务器。

终端3(“客户端B”):

nc -v -4 127.0.0.1 1234


此连接填充了服务器listen()队列中的一个空槽。它不是accept() ed。
现在,按Ctrl+C取消此nc。这将从问题创建CLOSE_WAIT状态。

观察ss输出

如果我们在另一个终端上观看sudo watch -n1 --exec ss -tapn 'sport = :1234'的同时运行上面的repro,我们可以观察上面每个步骤之后的状态:

# 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))


在最后一步中,我们已经可以观察到Process连接为空,这是因为连接确实建立了--但只与服务器内核建立了连接,而不是服务器进程,因为进程还没有accept()连接。
内核为我们执行TCP SYN-ACK-ACK握手,因此在accept()发生之前,在内核中建立了TCP连接。
现在,在客户端B断开连接后,我们看到没有进程的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输出

netstatsudo watch -n1 'netstat -antpe | grep 1234'
我们看到了更多的行,因为netstat不能做ss提供的方便的'sport = :1234'过滤,所以我们也看到了客户端套接字:

# 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          -


同样,这里我们将第一个查找的-作为ESTABLISHED连接的PID/Program name
这也回答了这个问题:

并且在客户端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实现更小的repro

由于nc可能会随着时间的推移而改变它的具体功能(例如,它进行的系统调用),这里有一个类似的Python TCP服务器,可以让你更清楚地了解服务器上发生了什么:

#!/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() s由listen()返回的套接字,或者
  • 服务器accept()是已经断开的连接。

接受已经断开的连接将成功:accept()将向服务器发送FD。这将无进程的CLOSE_WAIT转换为CLOSE_WAITwith 进程。服务器现在可以在FD上调用close()以关闭连接并解析CLOSE_WAIT状态。
listen()返回的套接字上调用close()会拆除整个内核队列,因此CLOSE_WAIT会立即消失。

摘要

  • 无进程CLOSE_WAIT s是进入listen()队列的连接,在我们的进程accept() s之前,它们被另一端发送TCP FIN * 终止,然后 * 被从listen()队列中取出。
  • 它们生活在内核中。
  • 它们没有关联的文件描述符(FD),因为为它们分配FD的accept()尚未发生。
  • 这就解释了为什么CLOSE_WAIT可以比文件描述符多。

问题中有3000个这样的无进程CLOSE_WAIT的问题表明服务器没有accept()连接。这可能是由于错误,或者因为服务器进程正在忙碌其他事情(例如垃圾收集或运行其他一些函数)。因此队列填满。服务器必须调用listen(, 3000)或更高版本。实际上,我可以看到ss -tlpn 'sport = :8000'中的Send-Q4096。当排队的客户端由于超时而最终给予时,排队的ESTABLISHED连接变成排队的CLOSE_WAIT连接。
因此,解决多达3000个CLOSE_WAIT连接的问题的下一步应该是找出服务器停止调用accept()的原因。

相关问题