详解五种IO模型
IO 模型是网络编程的基础,几乎所有的高性能的中间件都会提到使用了高效的 IO 模型(Redis、Kafka、Tomcat、Nginx 等)。
前言
Unix 系统下的五种基本 I/O 模型大家应该都有所耳闻,分别是:
- blocking I/O(同步阻塞IO,BIO)
- nonblocking I/O(同步非阻塞IO,NIO)
- I/O multiplexing (I/O多路复用)
- signal driven I/O(信号驱动I/O)
- asynchronous I/O(异步I/O,AIO)
一次网络 IO 的流程如下:

真正的I/O过程,主要分为下面两个阶段,所有 I/O 模型的区别就在这两个阶段上。
- 用户线程等待内核将数据从网卡拷贝到内核空间。
- 内核将数据从内核空间拷贝到用户空间。
同步阻塞 IO
用户线程发起 read 调用后就阻塞了,让出 CPU。内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒。

多线程可以提高 BIO 的吞吐量,因为在阻塞等待数据返回用户态的过程中 CPU 可以执行其它线程的任务。
一般为了避免过多的创建线程占据系统资源,系统中会使用"线程池"或者"连接池"减少创建和销毁线程的频率,让空闲的线程重新承担新的执行任务,维持一个合理的线程数量,降低系统开销。
但由于 BIO 每个线程负责一份 IO 请求的特性,当 IO 请求数量过大时就无能为力了~
BIO 在 IO 的两阶段都是阻塞的。
同步非阻塞 IO
用户线程不断的发起 read 调用,数据没到内核空间时,每次都返回失败,直到数据到了内核空间,这一次 read 调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒。

所以在非阻塞式 IO 中,用户进程其实是需要不断地主动询问 kernel 数据准备好了没有,但是这样采用轮询方式,会导致系统上下文切换开销很大,大幅度提高 CPU 占用率。
因此,单独使用非阻塞 I/O 模型的效率并不高。而且随着并发量的提升,非阻塞 I/O 会存在严重的性能浪费。
该模型轮询的目的只是检测"单个 IO 数据是否已经就绪",操作系统提供了更为高效的检测接口,一次检测多个 IO 数据是否已经就绪,这就是接下来的 IO 多路复用。
同步非阻塞 IO 是一阶段非阻塞,二阶段阻塞。
多路复用 IO
多路复用实现了一个线程处理多个 I/O 句柄的操作,这种 IO 方式也被称为事件驱动 IO(event driven IO)。
- 多路 指的是多个数据通道
- 复用 指的是使用一个或多个固定线程来处理每一个 Socket。
大名鼎鼎的 select、poll、epoll 就是该模式的不同实现。

多个的进程的 IO 可以注册到一个复用器(selector)上,然后用一个进程调用 select,select 会监听所有注册进来的 IO。
如果 selector 所有监听的 IO 在内核缓冲区都没有可读数据,select 调用进程会被阻塞(也可以设置超时时间,超过后就返回);同时,kernel 会"监视"本次 select 负责的 socket,如果任何一个 socket 中的数据准备好了,select 就会返回;之后就由业务进程对有 IO 事件发生的 socket 进行处理了。
多路复用 IO 模型只有一个 select 调用进程被阻塞,它也是非阻塞 IO,因为用户线程阻塞在 select 方法上,不像其他 IO 阻塞在 read write 方法调用。
多路复用解决了同步阻塞 I/O 和同步非阻塞 I/O 最大的问题(即并发量上来后也无需创建多个进程),单个 process 就可以同时处理多个网络连接的 IO。不过如果处理的 IO 数不多的情况下,使用多路复用 IO 的 web server 不一定比使用 池化+BIO 的 web server 性能更好,可能延迟还更大。考虑极端情况下,只有一个IO,多路复用需要 2 次系统调用(select + recvfrom),而BIO只需要 1 次系统调用(recvfrom)。
所以,多路复用 IO 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
信号驱动 IO
在使用信号驱动 I/O 时,当数据准备就绪后,内核通过发送一个 SIGIO 信号通知应用进程,应用进程就可以开始读取数据了。相比同步非阻塞 IO 来说,它不需要线程不断去轮询数据是否已经准备就绪。

注意在"数据由内核空间拷贝到用户空间"阶段,它仍然是"阻塞"的。
异步 IO
用户线程发起 read 调用的同时注册一个回调函数,read 立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。

AIO最重要的一点是 从内核缓冲区拷贝数据到用户态缓冲区的过程也是由系统异步完成,应用进程只需要在指定的数组中引用数据即可。
AIO 与信号驱动 I/O 的主要区别:信号驱动 I/O 由内核通知何时可以开始一个 I/O 操作,而异步 I/O 由内核通知 I/O 操作何时已经完成。
AIO 是真正的异步模型,它不会对请求进程产生任何的阻塞。
小结
阻塞与非阻塞
日常使用过程中,我们往往把 同步I/O 等同于 阻塞I/O,异步I/O 等同于 非阻塞I/O,但严格意义来说,这两组概念还是有很大的区别的。
结合 I/O模型 来说,阻塞I/O 会一直 block 对应的进程直到操作完成,而 非阻塞IO 在"等待数据从网卡拷贝到内核"阶段会立刻返回。
所以我们一般认为,阻塞I/O 只有 BIO,另外四个模型都是属于 非阻塞I/O。
同步与异步
根据 POSIX 的定义:
- 同步I/O : A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- 异步I/O : An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于 同步I/O 做 "IO operation" 的时候会将 process 阻塞。
那么按照这个定义,BIO,NIO,IO多路复用、信号驱动IO 四种模型都属于 同步IO。因为它们在 IO 的第二阶段,真正执行"数据拷贝"的阶段,都是"阻塞"的。