close()没有正确关闭套接字。

29
我有一个多线程服务器(线程池),处理大量请求(单个节点高达500 /秒),使用20个线程。有一个监听器线程接受传入连接并将其排队等待处理程序线程处理。一旦响应准备就绪,线程然后向客户端写出并关闭套接字。一切都很好,直到最近,一个测试客户端程序在读取响应后开始随机挂起。经过深入挖掘,似乎服务器的close()没有实际断开套接字。我已经添加了一些调试打印代码,其中包括文件描述符号码,我得到了这种类型的输出。
Processing request for 21
Writing to 21
Closing 21

调用close()方法的返回值应为0,否则会打印另一条调试语句。在使用出现问题的客户端时,lsof工具显示建立了一个连接。
服务器端:
8160 root 21u IPv4 32754237 TCP localhost:9980->localhost:47530 (已建立连接)
客户端:
17747 root 12u IPv4 32754228 TCP localhost:47530->localhost:9980 (已建立连接)
看起来,服务器没有向客户端发送关闭序列,此状态一直挂起,直到客户端被杀死,导致服务器处于关闭等待状态。
服务器端:
8160 root 21u IPv4 32754237 TCP localhost:9980->localhost:47530 (CLOSE_WAIT状态)
如果客户端指定了超时时间,它将会超时而不是挂起。我也可以手动运行...
call close(21)

在服务器上使用gdb,客户端将会断开连接。这种情况可能会发生50,000次请求中的一次,但也可能会持续很长时间。
Linux版本:2.6.21.7-2.fc8xen Centos版本:5.4(最终版)
套接字操作如下所示。
服务器:
int client_socket;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);  

while(true) {
  client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len);
  if (client_socket == -1)
    continue;
  /*  insert into queue here for threads to process  */
}

接下来,线程将获取套接字并构建响应。

/*  get client_socket from queue  */

/*  processing request here  */

/*  now set to blocking for write; was previously set to non-blocking for reading  */
int flags = fcntl(client_socket, F_GETFL);
if (flags < 0)
  abort();
if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0)
  abort();

server_write(client_socket, response_buf, response_length);
server_close(client_socket);

server_write 和 server_close。

void server_write( int fd, char const *buf, ssize_t len ) {
    printf("Writing to %d\n", fd);
    while(len > 0) {
      ssize_t n = write(fd, buf, len);
      if(n <= 0)
        return;// I don't really care what error happened, we'll just drop the connection
      len -= n;
      buf += n;
    }
  }

void server_close( int fd ) {
    for(uint32_t i=0; i<10; i++) {
      int n = close(fd);
      if(!n) {//closed successfully                                                                                                                                   
        return;
      }
      usleep(100);
    }
    printf("Close failed for %d\n", fd);
  }

客户端:

客户端使用的是libcurl v7.27.0

CURL *curl = curl_easy_init();
CURLcode res;
curl_easy_setopt( curl, CURLOPT_URL, url);
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, write_callback );
curl_easy_setopt( curl, CURLOPT_WRITEDATA, write_tag );

res = curl_easy_perform(curl);

没有花哨的东西,只是一个基本的curl连接。客户端在tranfer.c(在libcurl中)挂起,因为套接字没有被认为已关闭。它正在等待来自服务器的更多数据。

到目前为止我尝试过的事情:

在关闭之前进行关闭

shutdown(fd, SHUT_WR);                                                                                                                                            
char buf[64];                                                                                                                                                     
while(read(fd, buf, 64) > 0);                                                                                                                                         
/*  then close  */ 
       

将SO_LINGER设置为1秒强制关闭

struct linger l;
l.l_onoff = 1;
l.l_linger = 1;
if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1)
  abort();

这些都没有起到任何作用。如有任何想法,将不胜感激。

编辑--最终发现这是由于队列库中的线程安全问题导致多个线程不适当处理套接字。


1
你确定没有其他线程在你调用 close 关闭套接字时可能正在使用它吗?你如何进行非阻塞读取? - David Schwartz
1
很抱歉,我刚刚登录这里并想起了这个问题。后来我发现在用于传递连接的队列中存在线程安全问题。这里没有错误。对于提供的错误信息,我感到非常抱歉。 - DavidMFrey
4个回答

79

这里是我在许多类Unix系统(例如SunOS 4,SGI IRIX,HPUX 10.20,CentOS 5,Cygwin)上使用的代码,用于关闭套接字:

int getSO_ERROR(int fd) {
   int err = 1;
   socklen_t len = sizeof err;
   if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len))
      FatalError("getSO_ERROR");
   if (err)
      errno = err;              // set errno to the socket SO_ERROR
   return err;
}

void closeSocket(int fd) {      // *not* the Windows closesocket()
   if (fd >= 0) {
      getSO_ERROR(fd); // first clear any errors, which can cause close to fail
      if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery
         if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL
            Perror("shutdown");
      if (close(fd) < 0) // finally call close()
         Perror("close");
   }
}

但是上述方法并不能保证所有缓冲写入都已发送。

优雅地关闭:我花了大约10年的时间才想出如何关闭套接字。但在接下来的10年中,我只是懒散地调用usleep(20000)来稍微延迟一下,以“确保”写缓冲区在关闭之前被刷新。显然这不是很聪明,因为:

  • 大部分情况下延迟时间太长了。
  • 有些时候延迟时间太短了——也许!
  • 信号SIGCHLD可能会发生以结束usleep()(但我通常会调用usleep()两次来处理这种情况——一种hack)。
  • 无法确定这是否有效。但如果a)硬重置完全可以接受,和/或b)您可以控制链接的两端,则这可能并不重要。

但进行适当的刷新实际上非常困难。使用SO_LINGER显然不是解决问题的方法;例如请参见:

SIOCOUTQ似乎只适用于Linux。

请注意,shutdown(fd,SHUT_WR)并不会停止写入,与其名称相反,也可能与man 2 shutdown的描述不同。

此代码flushSocketBeforeClose()等待读取零字节,或者直到计时器过期。函数haveInput()是select(2)的简单包装器,并设置为最多阻塞1/100秒。

bool haveInput(int fd, double timeout) {
   int status;
   fd_set fds;
   struct timeval tv;
   FD_ZERO(&fds);
   FD_SET(fd, &fds);
   tv.tv_sec  = (long)timeout; // cast needed for C++
   tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t'

   while (1) {
      if (!(status = select(fd + 1, &fds, 0, 0, &tv)))
         return FALSE;
      else if (status > 0 && FD_ISSET(fd, &fds))
         return TRUE;
      else if (status > 0)
         FatalError("I am confused");
      else if (errno != EINTR)
         FatalError("select"); // tbd EBADF: man page "an error has occurred"
   }
}

bool flushSocketBeforeClose(int fd, double timeout) {
   const double start = getWallTimeEpoch();
   char discard[99];
   ASSERT(SHUT_WR == 1);
   if (shutdown(fd, 1) != -1)
      while (getWallTimeEpoch() < start + timeout)
         while (haveInput(fd, 0.01)) // can block for 0.01 secs
            if (!read(fd, discard, sizeof discard))
               return TRUE; // success!
   return FALSE;
}

使用示例:

   if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s
       printf("Warning: Cannot gracefully close socket\n");
   closeSocket(fd);
在上述代码中,我的getWallTimeEpoch()类似于time()Perror()perror()的一个包装器。

编辑:一些评论:

  • 我的第一个承认有点尴尬。OP和Nemo质疑在关闭之前清除内部的so_error的必要性,但我现在无法找到任何参考资料支持这一点。所涉及的系统是HPUX 10.20。在失败的connect()之后,仅调用close()并没有释放文件描述符,因为系统希望向我传递一个未完成的错误。但是,像大多数人一样,我从来没有费心去检查close的返回值。所以我最终用光了文件描述符(ulimit -n),这最终引起了我的注意。

  • (非常小的问题)一个评论者反对shutdown()中硬编码的数字参数,而不是使用例如SHUT_WR作为1。最简单的答案是Windows使用不同的#defines/enums例如SD_SEND。许多其他作者(如Beej)也使用常量,许多遗留系统也是如此。

  • 此外,在我的所有套接字上,我总是总是设置FD_CLOEXEC,因为在我的应用程序中,我永远不希望它们传递给一个子进程,并且更重要的是,我不希望一个挂起的子进程影响我。

设置CLOEXEC的示例代码:

   static void setFD_CLOEXEC(int fd) {
      int status = fcntl(fd, F_GETFD, 0);
      if (status >= 0)
         status = fcntl(fd, F_SETFD, status | FD_CLOEXEC);
      if (status < 0)
         Perror("Error getting/setting socket FD_CLOEXEC flags");
   }

5
我希望我能给这个点赞两次。这是我在实际情况中看到的第二个正确关闭的套接字示例。 - grieve
2
我认为应该使用相应的宏 SHUT_RD 等来操作 shutdown - Jens Gustedt
1
阅读关于 TCP 的绝妙的 FINWAIT 功能的相关内容。 - Steve-o
1
你的代码修复了我的客户端问题,即在被服务器断开连接后无法立即重新连接,因为客户端在确认 FIN 之前就发送了 SYN。 - Philippe A.
2
以防其他人也在尝试弄清楚getSO_ERROR()如何有助于解决问题:调用带有SO_ERRORgetsockopt将首先获取错误状态,然后重置它。对我来说,这些信息并不容易找到,我也不确定它是否可移植。以下手册记录了此行为:https://linux.die.net/man/3/getsockopt 但是我的发行版上相同的手册(man 3 getsockopt)没有记录(RHEL8)。 - psq
显示剩余5条评论

2

Joseph Quinsey给出了很好的答案。我对haveInput函数有一些评论。想知道select返回一个你没有包含在集合中的fd的可能性有多大。在我看来,这将是一个重大的操作系统错误。如果我为select函数编写单元测试,那就是我要检查的事情,而不是在普通应用程序中检查。

if (!(status = select(fd + 1, &fds, 0, 0, &tv)))
   return FALSE;
else if (status > 0 && FD_ISSET(fd, &fds))
   return TRUE;
else if (status > 0)
   FatalError("I am confused"); // <--- fd unknown to function

我的另一个评论涉及如何处理 EINTR。理论上,如果 select 不断返回 EINTR 错误,你可能会陷入无限循环,因为该错误让循环重新开始。然而,考虑到非常短的超时时间(0.01),这似乎极不可能发生。但是,我认为处理这种情况的适当方式是向调用者(flushSocketBeforeClose)返回错误。只要超时时间没有过期,调用者可以继续调用haveInput,对于其他错误则宣布失败。

附加说明 #1

flushSocketBeforeCloseread 返回错误时不会快速退出。它将保持循环直到超时结束。你不能依赖 haveInput 内部的 select 来预测所有错误。read 本身就有错误(例如: EIO)。

     while (haveInput(fd, 0.01)) 
        if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop
           return TRUE; 

1

在我看来,这似乎是您的Linux发行版中的一个错误。

GNU C库文档说:

当您使用完套接字后,可以使用close关闭其文件描述符。

没有关于清除任何错误标志或等待数据刷新或任何类似事情的说明。

您的代码没问题;您的操作系统有一个错误。


我接受这个答案,因为将我的线程安全队列更改为使用信号量而不是pthread条件已经不可思议地(至少对我来说)解决了这个问题。 - DavidMFrey
3
“Nothing about clearing any error flags or waiting for the data to be flushed or any such thing.” 可以翻译为“没有关于清除任何错误标志、等待数据刷新或任何类似情况的内容。” 可能可以认为“等待数据刷新”属于“当您使用套接字结束时”的范畴。 - Lightness Races in Orbit
2
@DavidMFrey 这意味着你的代码很可能存在逻辑错误、bug或竞争条件,而不是操作系统 bug。 - nos
1
这个假设极不可能发生。如果 close() 没有起作用,那么什么都不会起作用。 - user207421
1
@Nemo,那完全是错误的。举一个它错误的例子,想象一下如果有两个描述符引用同一个套接字。在任何一个描述符上调用close不会关闭套接字。 - David Schwartz
显示剩余5条评论

0

包含: #include <unistd.h>

这应该有助于解决close();问题


你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接