【问题标题】:Socket recv() hang on large message with MSG_WAITALL套接字 recv() 挂在带有 MSG_WAITALL 的大消息上
【发布时间】:2012-01-18 05:11:03
【问题描述】:

我有一个应用程序从服务器读取大文件并经常在特定机器上挂起。在RHEL5.2下成功运行了很长时间。我们最近升级到 RHEL6.1,现在它经常挂起。

我创建了一个可以重现问题的测试应用。它挂起大约 100 次中的 98 次。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/time.h>

int mFD = 0;

void open_socket()
{
  struct addrinfo hints, *res;
  memset(&hints, 0, sizeof(hints));
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_family = AF_INET;

  if (getaddrinfo("localhost", "60000", &hints, &res) != 0)
  {
    fprintf(stderr, "Exit %d\n", __LINE__);
    exit(1);
  }

  mFD = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

  if (mFD == -1)
  {
    fprintf(stderr, "Exit %d\n", __LINE__);
    exit(1);
  }

  if (connect(mFD, res->ai_addr, res->ai_addrlen) < 0)
  {
    fprintf(stderr, "Exit %d\n", __LINE__);
    exit(1);
  }

  freeaddrinfo(res);
}

void read_message(int size, void* data)
{
  int bytesLeft = size;
  int numRd = 0;

  while (bytesLeft != 0)
  {
    fprintf(stderr, "reading %d bytes\n", bytesLeft);

    /* Replacing MSG_WAITALL with 0 works fine */
    int num = recv(mFD, data, bytesLeft, MSG_WAITALL);

    if (num == 0)
    {
      break;
    }
    else if (num < 0 && errno != EINTR)
    {
      fprintf(stderr, "Exit %d\n", __LINE__);
      exit(1);
    }
    else if (num > 0)
    {
      numRd += num;
      data += num;
      bytesLeft -= num;
      fprintf(stderr, "read %d bytes - remaining = %d\n", num, bytesLeft);
    }
  }

  fprintf(stderr, "read total of %d bytes\n", numRd);
}

int main(int argc, char **argv)
{
  open_socket();

  uint32_t raw_len = atoi(argv[1]);
  char raw[raw_len];

  read_message(raw_len, raw);

  return 0;
}

我的测试中的一些笔记:

  • 如果“localhost”映射到环回地址 127.0.0.1,应用程序会挂起对 recv() 的调用并且永远不会返回。
  • 如果“localhost”映射到机器的ip,从而通过以太网接口路由数据包,则应用程序成功完成。
  • 当我遇到挂起时,服务器会发送“TCP 窗口已满”消息,客户端会以“TCP ZeroWindow”消息进行响应(参见图片和附加的 tcpdump 捕获)。从这一点开始,它永远挂起,服务器发送保持活动状态,客户端发送 ZeroWindow 消息。客户端似乎从不展开其窗口,以允许完成传输。
  • 在挂起期间,如果我检查“netstat -a”的输出,服务器发送队列中有数据,但客户端接收队列为空。
  • 如果我从 recv() 调用中删除 MSG_WAITALL 标志,应用程序将成功完成。
  • 只有在一台特定机器上使用环回接口时才会出现挂起问题。我怀疑这可能都与时间依赖性有关。
  • 当我减小“文件”的大小时,发生挂起的可能性会降低

可在此处找到测试应用的源代码:

Socket test source

可以在此处找到来自环回接口的 tcpdump 捕获:

tcpdump capture

我通过发出以下命令重现该问题:

>  gcc socket_test.c -o socket_test
>  perl -e 'for (1..6000000){ print "a" }' | nc -l 60000
>  ./socket_test 6000000

这看到发送到测试应用程序的 6000000 字节尝试使用对 recv() 的单个调用来读取数据。

我很想听听任何关于我可能做错了什么的建议或任何其他调试问题的方法。

【问题讨论】:

    标签: c linux sockets networking tcp


    【解决方案1】:

    考虑以下两个可能的规则:

    1. 接收方可能会等待发送方发送更多信息,然后才能收到已发送的内容。

    2. 发送方可能会等待接收方接收到已经发送的内容,然后再发送更多内容。

    我们可以拥有这些规则中的任何一个,但我们不能同时拥有这两个规则。

    为什么?因为如果允许接收者等待发送者,那就意味着发送者不能等待接收者接收到之后再发送更多,否则就会死锁。如果允许发送者等待接收者,则意味着接收者不能等待发送者发送,否则就会死锁。

    如果这两件事同时发生,我们就会陷入僵局。在接收者收到已经发送的内容之前,发送者不会再发送,除非发送者发送更多,否则接收者不会收到已经发送的内容。轰隆隆。

    TCP 选择规则 2(原因应该很明显)。因此它不能支持规则 1。但是在您的代码中,您是接收者,您正在等待发送者发送更多内容,然后才能收到已经发送的内容。所以这会死锁。

    【讨论】:

      【解决方案2】:

      MSG_WAITALL应该阻塞,直到收到所有数据。来自manual page on recv

      此标志请求操作阻塞,直到满足完整请求。

      但是,网络堆栈中的缓冲区可能不足以容纳所有内容,这就是服务器上出现错误消息的原因。客户端网络堆栈根本无法容纳那么多数据。

      解决方案是增加缓冲区大小(SO_RCVBUF 选项改为setsockopt),将消息拆分成更小的部分,或者接收更小的块将其放入您自己的缓冲区中。最后一个是我推荐的。

      编辑:我在您的代码中看到您已经按照我的建议进行了操作(使用自己的缓冲读取较小的块),因此只需删除 MSG_WAITALL 标志,它应该可以工作。

      哦,当recv 返回零时,这意味着另一端已经关闭了连接,你也应该这样做。

      【讨论】:

        猜你喜欢
        • 2012-08-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-08-18
        • 2015-01-28
        相关资源
        最近更新 更多