我有一个应用程序,它从服务器读取大文件,并经常挂在一台特定的机器上。它在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 %dn", __LINE__);
exit(1);
}
mFD = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (mFD == -1)
{
fprintf(stderr, "Exit %dn", __LINE__);
exit(1);
}
if (connect(mFD, res->ai_addr, res->ai_addrlen) < 0)
{
fprintf(stderr, "Exit %dn", __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 bytesn", 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 %dn", __LINE__);
exit(1);
}
else if (num > 0)
{
numRd += num;
data += num;
bytesLeft -= num;
fprintf(stderr, "read %d bytes - remaining = %dn", num, bytesLeft);
}
}
fprintf(stderr, "read total of %d bytesn", 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零窗口"消息(见图片和附加的tcpdump捕获)。从这一点开始,它将永远挂起,服务器发送keep-alive消息,客户端发送ZeroWindow消息。客户端似乎永远不会扩展它的窗口,允许传输完成。
- 在挂起期间,如果我检查"netstat -a"的输出,服务器发送队列中有数据,但客户端接收队列为空。
- 如果我从recv()调用中删除MSG_WAITALL标志,则应用程序成功完成。
- 挂起问题只在一台特定机器上使用环回接口时出现。我怀疑这可能都与时间依赖有关。
- 当我降低'文件'的大小时,发生挂起的可能性降低了
测试应用程序的源代码可以在这里找到:
Socket测试源
可以在这里找到从环回接口捕获的tcpdump:
tcpdump捕获我通过发出以下命令重现了这个问题:
> gcc socket_test.c -o socket_test
> perl -e 'for (1..6000000){ print "a" }' | nc -l 60000
> ./socket_test 6000000
这将看到6000000字节被发送到测试应用程序,测试应用程序试图通过调用recv()来读取数据。
我很乐意听到任何关于我可能做错的地方或任何进一步调试问题的方法的建议。
MSG_WAITALL
应该阻塞,直到接收到所有数据。从手册页上的recv:
该标志要求操作阻塞,直到满足完整的请求。
然而,网络堆栈中的缓冲区可能不足以包含所有内容,这就是服务器上出现错误消息的原因。客户端网络堆栈根本无法容纳那么多数据。
解决方案是增加缓冲区大小(SO_RCVBUF
选项到setsockopt
),将消息分成更小的块,或者接收更小的块并将其放入自己的缓冲区中。最后一种是我推荐的。
编辑:我在你的代码中看到你已经做了我建议的(用自己的缓冲读取较小的块),所以只是删除MSG_WAITALL
标志,它应该工作。
哦,当recv
返回0时,这意味着另一端已经关闭了连接,您也应该这样做
考虑以下两个可能的规则:
-
接收方可以等待发送方发送更多的消息,然后再接收已经发送的消息。
-
发送方可以等待接收方接收到已经发送的内容后再发送。
我们可以有这两个规则中的任何一个,但我们不能同时有这两个规则。
为什么?因为如果允许接收方等待发送方,那就意味着发送方不能等到接收方接收后再发送,否则就会死锁。如果允许发送方等待接收方,那就意味着接收方不能等待发送方发送,否则就会死锁。
如果这两件事同时发生,就会发生死锁。发送方只有在接收方收到已发送的内容后才会发送更多内容,而接收方只有在发送方发送更多内容后才会收到已发送的内容。繁荣。
TCP选择规则2(原因应该很明显)。因此不能支持规则1。但是在你的代码中,你是接收者,在你收到已经发送的信息之前,你要等待发送者发送更多的信息。这将会死锁