几种常见的I/O模型及其区别

首先,介绍几种常见的I/O模型及其区别,如下:

  • 阻塞式模型(Blocking I/O)
  • 非阻塞式模型(Nonblocking I/O)
  • 多路复用IO(I/O Multiplexing (select & poll))
  • 信号驱动IO模型(Signal Driven I/O (sigio))
  • 异步IO模型(Asynchronous I/O (the POSIX aio_functions))

阻塞式模型(Blocking I/O)

所谓阻塞型接口是指系统调用(一般是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

socket的接口是阻塞的(blocking),在线程被阻塞期间,线程将无法执行任何运算或响应任何的网络请求,这给多客户机、多业务逻辑的网络编程带来了挑战。

 

首先用户进程调用recfrom()转入内核,注意内核有两个过程,等待数据将数据从内核拷贝到用户空间。直到最后复制完成后,recfrom()才返回。此过程一直是阻塞的。

非阻塞式模型(Nonblocking I/O)

非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。

从应用程序的角度来说,blocking read 调用会延续很长时间。在内核执行读操作和其他工作时,应用程序会被阻塞。

非阻塞的IO可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止。

另一个问题,在循环调用非阻塞IO的时候,将大幅度占用CPU,所以一般使用select等来检测”是否可以操作“。

非阻塞型IO的示意图如下:

可以看见,如果直接操作它,那就是个轮询。直到内核缓冲区有数据。

多路复用IO(I/O Multiplexing (select & poll))

支持I/O复用的系统调用有select、poll、epoll、kqueue等,这里Select监听的socket都是Non-blocking的,所以在do_read() do_write()中对返回为EAGAIN/WSAEWOULDBLOCK都做了处理。

从代码中可以看出使用Select返回后,仍然需要轮训再检测每个socket的状态(读、写),这样的轮训检测在大量连接下也是效率不高的。因为当需要探测的句柄值较大时,select () 接口本身需要消耗大量时间去轮询各个句柄。

很多操作系统提供了更为高效的接口,如 linux 提供 了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll 。如果需要实现更高效的服务器程序,类似 epoll 这样的接口更被推荐。遗憾的是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难。

最常见的I/O复用模型,select。示意图如下:

select先阻塞,有活动套接字才返回。与阻塞式模型(blocking I/O)相比,select会有两次系统调用,但是select能处理多个套接字。

信号驱动IO模型(Signal Driven I/O (sigio))

使用信号,让内核在描述符就绪时发送SIGIO信号通知应用程序,称这种模型为信号驱动式I/O(signal-driven I/O)。

首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说进程没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。随后就可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理,也可以立即通知主循环,让它读取数据报。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行 ,只要等到来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

只有UNIX系统支持,与多路复用IO(I/O Multiplexing (select & poll))相比,它的优势是,免去了select的阻塞与轮询,当有活跃套接字时,由注册的处理程序(handler)处理。

异步IO模型(Asynchronous I/O (the POSIX aio_functions))

异步I/O(asynchronous I/O)由POSIX规范定义。演变成当前POSIX规范的各种早起标准所定义的实时函数中存在的差异已经取得一致。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

我们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,并且在等待I/O完成期间,我们的进程不被阻塞。本例子中我们假设要求内核在操作完成时产生某个信号,该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动I/O模型。

很少有Unix系统支持,Windows的IOCP则是此模型。完全异步的I/O复用机制,因为纵观上面其它四种模型,至少都会在由内核拷贝到用户进程时阻塞。而该模型是当拷贝完成后才通知用户进程,可见是纯异步的。好像只有Windows的完成端口是这个模型,效率也很出色。

以上五种模型的比较

可以看出,越往后,阻塞越少,理论上效率也是最优。


5种模型的比较比较清晰了,剩下的就是把select,epoll,iocp,kqueue按号入座那就OK了。

select和iocp分别对应第3种与第5种模型,那么epoll与kqueue呢?其实也于select属于同一种模型,只是更高级一些,可以看作有了第4种模型的某些特性,如callback机制。

那么,为什么epoll,kqueue比select高级?

答案是,他们无轮询。因为他们用callback取代了。想想看,当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

windows or Unix (IOCP or kqueue/epoll)?

诚然,Windows的IOCP非常出色,目前很少有支持asynchronous I/O的系统,但是由于其系统本身的局限性,大型服务器还是在UNIX下。而且正如上面所述,kqueue/epoll 与 IOCP相比,就是多了一层从内核copy数据到应用层的阻塞,从而不能算作asynchronous I/O类。但是,这层小小的阻塞无足轻重,kqueue与epoll已经做得很优秀了。

提供一致的接口,IO Design Patterns

实际上,不管是哪种模型,都可以抽象一层出来,提供一致的接口,广为人知的有ACE,Libevent这些,他们都是跨平台的,而且他们自动选择最优的I/O复用机制,用户只需调用接口即可。说到这里又得说说2个设计模式,Reactor and Proactor。Libevent是Reactor模型,ACE提供Proactor模型。实际都是对各种I/O复用机制的封装。

Java nio包是什么I/O机制?

我曾天真的认为java nio封装的是IOCP。。现在可以确定,目前的java本质是select()模型,可以检查/jre/bin/nio.dll得知。至于java服务器为什么效率还不错。。我也不得而知,可能是设计得比较好吧。。-_-。

总  结

  1. 只有IOCP是asynchronous I/O,其他机制或多或少都会有一点阻塞。
  2. select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
  3. epoll, kqueue是Reacor模式,IOCP是Proactor模式。
  4. java nio包是select模型。。