Linux上的五种IO模型及两种事件处理模式
五种IO模型
阻塞IO(blocking)
调用者调用了这个函数,等待这个函数返回,期间什么也不做,不停地去检查这个函数有没有返回,必须等这个函数返回才能进行下一个动作。
非阻塞IO(NIO)
非阻塞等待,每隔一段时间就去检测IO时间是否就绪。没有就绪就可以做其他事。非阻塞IO执行系统调用总是立即返回,不管事件是否已经发生,如事件没有发生,返回-1。此时可根据errno区分这两种情况,对于accept,recv和send事件未发生时,errno通常被设置为EAGAIN。
IO复用(multiplexing)
linux用select/poll/epoll函数实现IO复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是,这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数据可读或者可写时,才真正调用IO操作函数。
信号驱动
Linux用套接字进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理事件。
内核在第一个阶段是异步,第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断地轮询,减少了系统API的调用次数,提高了效率。
异步IO
调用aio_read函数告诉内核描述字符缓冲区指针和缓冲区的大小、文件偏移以及通知的方式,然后立即返回,当内核数据拷贝到缓冲区后,再通知应用程序。
两种事件处理模式
服务器程序通常需要处理三类事件:IO事件、信号、定时事件。这对应两种事件处理模式:Reactor、Proactor。同步IO模型通常用于实现Reactor模式,异步IO模型通常用于实现Proactor模式。
Reactor模式
要求主线程只负责监听文件描述符上是否有事情发生,有的话立即将该事件通知工作线程,将socket可读可写事件放到请求队列中,交给工作线程处理。除此之外,主线程不做其他任何实质性的事情。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步IO(epoll为例)实现Reactor模式的工作流程如下:
- 主线程往epoll内核事件表中注册socket上的读就绪事件;
- 主线程调用epoll_wait等待socket上有数据可读;
- 当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列;
- 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中则册该socket上的写就绪事件;
- 主线程调用epoll_wait等待socket写事件;
- 当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列;
- 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
Proactor模式
Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻辑。使用异步 I/O 模型(以 aio_read 和 aio_write 为例)实现的 Proactor 模式的工作流程是:
- 主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例)。
- 主线程继续处理其他逻辑。
- 当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
- 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序。
- 主线程继续处理其他逻辑。
- 当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。