24-总结
63.6 总结
本章我们探究了针对标准I/O模型之外的其他几种可选的I/O模型。它们是:I/O多路复用(select()和poll())、信号驱动I/O以及Linux专有的epoll API。所有这些机制都允许我们监视多个文件描述符,以查看哪个文件描述符上可执行I/O操作。需要注意的是,所有这些机制并不实际执行I/O操作。相反,一旦发现某个文件描述符处于就绪态,我们仍然采用传统的I/O系统调用来完成实际的I/O操作。
I/O 多路复用机制中的select()和poll()能够同时监视多个文件描述符,以查看哪个文件描述符上可执行 I/O 操作。在这两个系统调用中,我们传递一个待监视的文件描述符列表给内核,之后内核返回一个修改过的列表以表明哪些文件描述符处于就绪态了。在每一次调用中都要传递完整的文件描述符列表,并且在调用返回后还要检查它们,这个事实表明当需要监视大量的文件描述符时,select()和 poll()的性能表现将变得很差。
信号驱动I/O允许一个进程在文件描述符处于I/O就绪态时接收到一个信号。要使用信号驱动I/O,我们必须为SIGIO信号安装一个信号处理例程,设定接收信号的属主进程,并在打开文件时设定O_ASYNC标志使得信号可以生成。相比I/O多路复用,当监视大量的文件描述符时信号驱动I/O有着显著的性能优势。Linux允许我们修改用来通知的信号,而如果我们采用实时信号的话,那么多个信号通知就可以排队处理。信号处理例程可以使用siginfo_t参数来确定产生信号的文件描述符以及发生事件的类型。
同信号驱动I/O一样,当监视大量的文件描述符时epoll也能提供高效的性能。epoll(以及信号驱动I/O)的性能优势源自内核能够“记住”进程正在监视的文件描述符列表这一事实(与之相反的是,select()和poll()都必须反复告诉内核哪些文件描述符需要监视)。相比于信号驱动I/O,epoll API还有些值得一提的优点:我们可以避免处理信号时的复杂流程,而且可以指定需要监视的I/O事件类型(例如输入或输出事件)。
本章中我们在水平触发通知和边缘触发通知之间做了严格区分。在水平触发通知模型下,只要当前文件描述符上可以进行I/O操作,我们就能得到通知。与之相反,在边缘触发通知模型下,只有自上一次监视以来,文件描述符上有发生I/O事件时才会通知我们。I/O多路复用采用的是水平触发通知模型;信号驱动I/O基本上是边缘触发通知模型;而epoll能够以任意一种方式工作(默认情况下是水平触发)。边缘触发通知通常都和非阻塞式I/O结合起来使用。
本章结尾部分我们探讨了一个经常会遇到的问题。那就是如何在监视多个文件描述符的同时等待信号的发送?对于这个问题,通常的解决方案是采用一种称为self-pipe的技巧,即信号处理例程写一个字节数据到管道中,代表管道读端的文件描述符包含在被监视的文件描述符集合中。SUSv3中定义了pselect(),这是select()的变种,它提供了解决这个问题的另一种方法。但是pselect()并没有包含在所有的UNIX实现中。Linux也提供了类似(但非标准)的ppoll()和epoll_pwait()接口。
更多信息
[Stevens et al., 2004]中介绍了I/O多路复用以及信号驱动I/O,尤其强调了这些机制在套接字上的使用。[Gammeo et al, 2004]是一篇比较select()、poll()和epoll之间性能表现的论文。
网络上有一个特别有趣的资源,地址为http://www.kegel.com/c10k.html。这就是由Dan Kegel所著的著名的“C10K问题”。在这个页面上作者探究了Web服务器端的开发者在设计能够同时处理上万个客户端的系统时会遇到的问题和困难,页面上还包含了其他相关信息的链接。