我有一个应用程序从服务器读取大型文件,并经常挂在特定的机器上。 它已经在RHEL5.2下成功运行了很长一段时间。 我们最近升级到RHEL6.1,现在它定期挂起。
我已经创build了一个重现问题的testing应用程序。 它挂起100次约98次。
#include #include #include #include #include #include #include #include #include #include #include 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; }
从我testing的一些笔记:
如果“localhost”映射到回送地址127.0.0.1,则应用程序挂起对recv()的调用,并且不返回。
如果“本地主机”映射到机器的IP,因此通过以太网接口路由数据包,应用程序成功完成。
当我遇到挂起时,服务器发送一个“TCP Window Full”消息,并且客户端响应“TCP ZeroWindow”消息(请参阅图像和附加的tcpdump捕获)。 从这一点来看,服务器发送keepalive和客户端发送ZeroWindow消息永远挂起。 客户似乎从来没有扩大其窗口,允许转移完成。
在挂起期间,如果我检查“netstat -a”的输出,则服务器中有数据发送队列,但客户端接收队列是空的。
如果我从recv()调用中删除MSG_WAITALL标志,则应用程序成功完成。
挂起的问题只出现在1台特定的机器上使用回送接口。 我怀疑这可能都与时间依赖性有关。
当我放弃'文件'的大小,挂起发生的可能性减less
testing应用程序的来源可以在这里find:
套接字testing源
从loopback接口捕获的tcpdump可以在这里find:
tcpdump捕获
我通过发出以下命令来重现此问题:
> gcc socket_test.c -o socket_test > perl -e 'for (1..6000000){ print "a" }' | nc -l 60000 > ./socket_test 6000000
这看到6000000字节发送到testing应用程序尝试读取数据使用recv()的单个调用。
我很乐意听到关于我可能做错了什么的build议或任何进一步debugging问题的方法。
应该阻塞MSG_WAITALL直到收到所有数据。 从recv手册页面 :
该标志请求操作块直到完整的请求被满足。
但是,网络堆栈中的缓冲区可能不足以包含所有内容,这是服务器上的错误消息的原因。 客户端网络堆栈根本不能容纳那么多的数据。
解决方法是增加缓冲区大小( SO_RCVBUF选项setsockopt ),将消息拆分成更小的块,或接收更小的块放入你自己的缓冲区。 最后是我会建议。
编辑:我在你的代码中看到,你已经做了我所建议的(用自己的缓冲区读取更小的块),所以只要删除MSG_WAITALL标志,它应该工作。
哦,当recv返回零时,这意味着另一端已经关闭了连接,你也应该这样做。
考虑这两个可能的规则:
接收者可能会在收到已发送的内容之前等待发送者发送更多内容。
发送者可以等待接收者在发送更多之前接收已经发送的内容。
我们可以有这些规则中的任何一个,但是我们不能同时拥有这些规则。
为什么? 因为如果接收方被允许等待发送方,这意味着发送方在发送之前不能等待接收方接收,否则我们会死锁。 而且如果发送者被允许等待接收者,那就意味着接收者不能等待发送者接收更多的发送,否则我们会死锁。
如果这两件事情同时发生,我们就陷入僵局。 在接收方收到已发送的内容之前,发送方不会再发送更多内容,除非发送方发送更多内容,否则接收方将不会收到已经发送的内容。 繁荣。
TCP选择规则2(原因应该是显而易见的)。 因此它不能支持规则1.但是在你的代码中,你是接收者,并且在你接收到已经发送的内容之前,你正在等待发送者发送更多的内容。 所以这会陷入僵局。