You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免无文件描述符的ESTABLISHED状态幽灵TCP连接

幽灵TCP套接字问题:无文件描述符却处于ESTABLISHED状态的原因与解决方法

我有一个多线程测试程序,在本地主机上创建大量TCP自连接。遇到的问题是,部分主动发起连接的套接字虽无关联文件描述符,却仍处于ESTABLISHED状态。以下是可复现的C++最小示例:

#include <cerrno>
#include <print>
#include <thread>

#include <netinet/in.h>
#include <poll.h>
#include <stdint.h>
#include <sys/socket.h>

void
do_accept(int lfd)
{
  sockaddr_in sin;
  socklen_t sinlen = sizeof(sin);
  int fd = accept(lfd, (sockaddr *) &sin, &sinlen);

  close(lfd);

  char buf[128];
  errno = 0;
  while (read(fd, buf, sizeof(buf)) > 0)
    ;
  close(fd);
}

void
do_poll(int cfd, int wake)
{
  pollfd pfds[2];
  pfds[0].fd = cfd;
  pfds[0].events = POLLIN;
  pfds[1].fd = wake;
  pfds[1].events = POLLIN;
  poll(pfds, 2, -1);
}

int
main()
{
  int lfd = socket(AF_INET, SOCK_STREAM, 0);
  sockaddr_in sin{};
  sin.sin_family = AF_INET;
  sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
  bind(lfd, (sockaddr *)&sin, sizeof(sin));
  listen(lfd, 5);

  socklen_t sinlen = sizeof(sin);
  getsockname(lfd, (sockaddr *)&sin, &sinlen);
  uint16_t port = ntohs(sin.sin_port);

  std::println("listening on port {}", port);
  std::thread acceptor(do_accept, lfd);

  int cfd = socket(AF_INET, SOCK_STREAM, 0);
  if (connect(cfd, (sockaddr *)&sin, sinlen)) {
    perror("connect");
    exit(1);
  }

  int selfpipe[2];
  pipe(selfpipe);
  std::thread poller(do_poll, cfd, selfpipe[0]);
  poll(nullptr, 0, 1000);

  //shutdown(cfd, SHUT_WR); // Works if you uncomment
  close(cfd);           // Would like this to send EOF
  acceptor.join();      // Would like this to return

  std::println("Will never get this far");
  write(selfpipe[1], "", 1);
  poller.join();
}

该程序会创建无关联文件描述符的“幽灵套接字”,可通过ss和lsof命令验证,例如:

$ lsof -c closepoll -nPa -i tcp -Tqs
COMMAND      PID USER FD   TYPE  DEVICE SIZE/OFF NODE NAME
closepoll 918275 user 5u  IPv4 8515786      0t0  TCP 127.0.0.1:41503->127.0.0.1:57248 (ESTABLISHED QR=0 QS=0)
$ ss -antp "sport 57248 or dport 57248"
State    Recv-Q    Send-Q       Local Address:Port        Peer Address:Port     Process                                                                         
ESTAB    0         0                127.0.0.1:57248          127.0.0.1:41503                                                                                    
ESTAB    0         0                127.0.0.1:41503          127.0.0.1:57248     users:((closepoll,pid=918275,fd=5))

尽管处于ESTABLISHED状态,但本地端口57248并无对应的文件描述符。当最后一个文件描述符关闭时,它难道不应该进入FIN_WAIT_1状态吗?我的问题是:是什么导致这些无关联文件描述符却仍处于ESTABLISHED状态的幽灵TCP套接字?如何避免这个问题?

更新 请注意,即使检查所有系统调用错误并立即终止程序,代码行为仍相同。更重要的是,无论系统调用是否出错,运行上述代码都会产生幽灵TCP套接字。因此我的问题依旧:是什么产生了这类幽灵TCP套接字,以及如何避免?


原因分析

  1. close与shutdown的核心行为差异
    close操作的对象是文件描述符,而非TCP连接本身。只有当某个套接字的所有关联文件描述符都被关闭时,内核才会触发TCP连接的关闭流程(发送FIN等)。如果有其他线程仍在引用该文件描述符的数值(比如你的poller线程还在对已关闭的fd执行poll操作),内核可能会延迟启动关闭流程,导致TCP连接停留在ESTABLISHED状态。

    而shutdown操作的对象是TCP连接,调用shutdown(cfd, SHUT_WR)会立即关闭连接的写方向,向对端发送FIN,无论有多少个文件描述符指向该套接字。这也是为什么你注释掉的那行代码能解决问题。

  2. TCP连接生命周期与进程文件描述符解耦
    内核会独立维护TCP连接的状态,直到完成四次握手或超时,即使进程已经关闭了所有关联的文件描述符。如果连接的一端未发送FIN,另一端会一直处于ESTABLISHED状态,直到内核的超时机制触发。在你的例子中,客户端未发送FIN,导致服务器端的read一直阻塞,连接状态无法推进。

解决方法

方法1:显式调用shutdown触发FIN发送

在关闭客户端套接字的文件描述符前,先调用shutdown关闭写方向,强制发送FIN:

shutdown(cfd, SHUT_WR); // 立即向服务器端发送FIN
close(cfd);

这样服务器端的read会返回0,结束循环并关闭连接,accept线程正常退出,主线程的join能顺利返回。

方法2:先终止所有引用该fd的线程

在关闭fd前,先通过selfpipe唤醒poller线程,确保没有线程再引用该fd,再执行close:

write(selfpipe[1], "", 1); // 唤醒poller线程
poller.join(); // 等待poller线程退出
close(cfd); // 此时关闭fd会触发FIN发送
acceptor.join();

这种方式能保证close操作时,套接字的所有文件描述符引用都已释放,内核会立即启动TCP关闭流程。

方法3:设置SO_LINGER选项(谨慎使用)

通过SO_LINGER选项让close立即发送RST(而非FIN),强制重置连接:

struct linger l{};
l.l_onoff = 1;
l.l_linger = 0; // 立即关闭,发送RST
setsockopt(cfd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));
close(cfd);

注意:这种方式会导致连接被强制重置,可能丢失未发送的数据,仅适用于不需要优雅关闭的场景。


内容的提问来源于stack exchange,提问作者user3188445

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 12:49:53