IO模型
I/O复用是最常使用的I/O通知机制。它指的是,应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。linux上常用的I/O复用函数是select,poll和epoll_wait。需要指出的是,I/O复用函数本身是阻塞的,它们能提高程序效率的原因在于它们具有同时监听多个I/O事件的能力。
SIGIO信号也可以用来报告I/O事件。我们可以为一个目标文件描述符制定宿主进程,那么被指定的宿主进程将捕获到SIGIO信号。这样,当目标文件描述符上有事件发生时,SIGNO信号的信号处理函数将被触发,我们也就可以在该信号处理函数中对目标文件描述符执行非阻塞I/O操作了。
从理论上说,阻塞I/O、I/O复用和信号驱动I/O都是同步I/O模型。因为这三种I/O模型中,I/O的读写操作,都是在I/O事件发生之后,由应用程序来完成的。而POSIX规范所定义的异步I/O模型则不同。
对异步I/O而言,用户可以直接对I/O执行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及I/O操作完成之后内核通知应用程序的方式。异步I/O的读写操作总是立即返回,而不论I/O是否是阻塞的,因为真正的读写操作已经由内核接管。也就是说,同步I/O模型要求用户代码自行执行I/O操作(将数据从内核缓冲区读入用户缓冲区,或将数据从用户缓冲区写入内核缓冲区),而异步I/O机制则由内核来执行I/O操作(数据在内核缓冲区和用户缓冲区之间的移动是由内核在”后台“完成的)。你可以这样认为,同步I/O向应用程序通知的是I/O就绪事件,而异步I/O向应用程序通知的是I/O完成事件。
linux环境中,aio.h头文件中定义的函数提供了对异步I/O的支持。
作为总结,我们将上面讨论的几种I/O模型的差异列于下图中
两种高效的事件处理模式
这两种高效的事件处理模式为:Reactor和Proactor
> 随着网络设计模式的兴起,Reactor和Proactor事件处理模式应运而生。同步I/O模型通常用于实现Reactor模式,异步I/O模型则用于实现Proactor模式。
Reactor模式
Reactor是这样一种模式,他要求主线程(I/O处理单元,下同)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元,下同)。
除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程是:
- 主线程往epoll内核事件表中注册socket上的度就绪事件。
- 主线程调用epoll_wait等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。
- 主线程调用epoll_wait等待socket可写
- 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
下图总结了Reactor模式的工作流程
图中,工作线程从请求队列中取出事件后,将根据事件的类型来决定如何处理它:对于可读事件,执行读操作和处理请求的操作;对于可写事件,执行写数据的操作。
因此上图所示的Reactor模式中,没必要区分所谓的“读工作线程” 和 “写工作线程”
参考书籍 《linux高性能服务器编程》