昨天为了比较UNIX socket跟TCP/IP socket的性能,写了一个echo server简单的比较了两者的性能,通过初步不详细的结果,能够看出UNIX的网络交互性能确实要比TCP/IP性能高。但是,除了这个问题解决了之后,还遇到了自己意想不到的收获。
echo server中的处理请求示例代码如下所示,这也是从unix网络编程第五章摘抄的代码 如下:
- void str_echo(int sockfd)
-
{
-
ssize_t n;
-
char buf[MAX_LEN];
-
-
again:
-
while ((n = read(sockfd, buf, MAX_LEN)) > 0)
-
{
-
write(sockfd, buf, n);
-
}
-
-
if (n < 0 && errno == EINTR)
-
goto again;
-
else if (n < 0)
-
perror("str_echo: read error:");
-
}
这段服务器端代码其实没什么问题,只是在write函数返回的时候没有做判断,但这个小瑕疵并不影响我后续说的主要问题。
这段代码在一般的情形下能够很好的服务客户端请求,使用telnet能够看到echo回来的字符串。
但是,在客户端请求一个大字符串长度的时候(比如10000000),这段程序就会造成整个客户端与服务器之间的死锁。
在socket中,一般都会为发送与接受双方向维护缓冲区,如果发送段发送数据时,如果缓冲区有空闲的地方,那么就是将数据放入到发送缓冲区中,如果缓冲区没有空闲区域,那么根据socket的类型决定相应操作:
- 如果socket是阻塞的write,那么write将阻塞,直到发送缓冲区有数据出现。
- 如果socket是非阻塞的write,那么write将立即返回,errno = EAGAIN或者EWOULDBLOCK,由调用方决定后续操作。
如果client端使用的是阻塞write(server_fd, big_buf, 10000000),写一个巨大的buffer,client端在内核部分会将这么大的buffer分批次发送,server这端接受到被拆分的数据后直接调用自己的write函数写回给client端,数据则是先到了server的发送缓冲区,然后经过网络又到了client的接受缓冲区,但是由于client段还在write函数中,并没有read,所以数据就只能停留在自己的接收缓冲区中。
这就像一个水龙头源源不断的出水,水流向一个封着口的管子中,水龙头必须把水放完了之后,才能打开管子的封口把水放出来。管子的容量是有限的,但是水的量是不确定的。如果水的量比较少,那么这套系统可以正常工作,如果水量大于管子的容量了,那么这套系统就死锁了。
- 首先,被填满的就是client的接受缓冲区,其实也不是填满,由于TCP的滑动窗口,当达到了窗口限制之后,发送端就不再发送了。也许缓冲区会比窗口大,这个没看过具体实现,尚不做硬性关联。总之,client接受被填满了。
- 然后,server端的发送缓冲区由于数据越来越多,又不能发给client,总之慢慢的就被填满了。带来的结果就是,server的程序被阻塞到write函数中了,这样导致server就不能够read了。
- 接着,server程序被阻塞到write函数中了,那么也自然就不会read了,慢慢的,慢慢的,server端的TCP接收窗口也就满了,再也不能从client端读数据了。管子就此也就满了三截了。
- 最后,因为server端的TCP接受窗口已经满了,client端的内核就不能将数据发给server段了,就只能在自己的发送缓冲区存了,渐渐的,渐渐的,client端的发送缓冲区也满了。但是,这个时候的write还没有返回,client端也阻塞在了write中。
- 最后的最后,系统就死锁了。:-),这应该是分布式中最简单的死锁了……
昨天遇到那个问题,今天用了一天的闲暇时光总算是把这个问题想通了。其实这个死锁问题还是典型的,满足死锁发生的各种条件。最根本原因就是系统的资源无法满足,然后又没能够合理的安排。最简单的解决办法就是客户端这边发一点,读一点,这个问题也就避免了,使得不会一次请求那么多的资源,这个问题也就解决了。
阅读(991) | 评论(0) | 转发(0) |