文章从我的个人博客备份而来
unix下有5种可用的io模型,它们分别是:
- 阻塞io
- 非阻塞io
- io复用
- 信号式驱动io
- 以及异步io
阻塞式io模型
一般来说,我们平时编程使用的最多的,就是阻塞式io模型。
举个例子,比如说我们有一个函数
struct hostent *gethostbyname(const char *name);
这个时候我们调用这个函数
gethostbyname("www.google.cn")
进程会一直阻塞,直到调用完成,或者调用失败。
阻塞式io.jpg
非阻塞式io
在类unix操作系统下,我们有:
fcntl(fd, F_SETFL, O_NONBLOCK);
这里的fd可以是文件描述符,也可以是套接字描述符。
假设我们调用了
recv(fd, buf, sizeof(buf), 0)
如果是服务端接收客户端的信息,假如客户端的数据没有准备好,服务端并不会被阻塞,而是会发起轮询。
轮询.jpg但是假如我们有多个客户端,都设为非阻塞式,那么我们会对多个客户端都发起轮询。这样非常的消耗资源。在某些特定的情况,我们会使用非阻塞式的模型
io复用模型
这里使用常见的select()来介绍:
复用io.jpg进程阻塞于select()调用,当条件准备好的时候,我们调用recv().
和阻塞式io比起来,这好像没什么区别,还多了一次select()调用。对于单个套接字来说,确实是这样。但是对于多个套接字来说效率会大大的提高。
假设我们有fd[0], fd[1], fd[2], .... fd[n] (n < 1024)
如果我们采用阻塞式io的话,这时我们遍历 fd数组,而在这个数组中fd[3]是准备好的,而fd[0], fd[1], fd[2]都没有准备好。我们会一直阻塞到直到fd[0],fd[1],fd[2]都准备好才会使用recv接收fd[3]的数据。
假如我们使用select(),遍历过fd数组以后确认fd[3]是准备好的则会先执行fd[3]。
下面是一段复用的示例程序
fd_set readset;
int i, n;
char buf[1024];
while (i_still_want_to_read()) {
int maxfd = -1;
FD_ZERO(&readset);
for (i=0; i < n_sockets; ++i) {
if (fd[i]>maxfd) maxfd = fd[i];
FD_SET(fd[i], &readset);
}
select(maxfd+1, &readset, NULL, NULL, NULL);
for (i=0; i < n_sockets; ++i) {
if (FD_ISSET(fd[i], &readset)) {
n = recv(fd[i], buf, sizeof(buf), 0);
if (n == 0) {
handle_close(fd[i]);
} else if (n < 0) {
if (errno == EAGAIN)
else
handle_error(fd[i], errno);
} else {
handle_input(fd[i], buf, n);
}
}
}
}
但是select有许多缺点,比如说它每次扫描都是线性扫描,当描述符变得很多以后,他的效率会非常的低。而且单个进程打开的fd数量是有限制的,默认是1024.而另一个unix提供的poll()函数同样也会随着套接字的增加效率降低。
在Linux2.6内核以后提供了epoll(),而freebsd也提供了kqueue。相比起poll(),select(),他们更加的优秀。
信号式驱动
假设我们还是服务器接收客户端的数据,信号式驱动是客户端先发送一个信号表示数据已经准备好,可以发送。这种方法的优势在于等待期间我们的进程不会被阻塞。
异步io模型
还是我们调用一个函数,异步io在执行时进程并不会被阻塞,而在执行完成之时,会传递一个信号来告知执行完成。
网友评论