Linux IO 模型
前言
本文会讲解Unix的五种 I/O 模型,并对各个模型做一个对比。
在分析 I/O 模型之前,首先要了解Unix中 I/O 的操作流程。
I/O 就是 Input和Output的缩写,所以整个 I/O 流程包含了输入和输出两个阶段。以进程的角度看着两个阶段:
- 第一阶段:内核准备数据的阶段,即当用户进程请求内核时,如果内核缓冲区没有数据,内核从硬件或者网络上读取数据到内核缓冲区的阶段。
- 第二阶段:内核拷贝数据的阶段,即数据到达内核缓冲区后,内核将数据从内核缓冲区拷贝到进程的这一过程。这个过程其实还是由内核提供的系统接口完成。
Unix的五种 I/O 模型简介:
- 阻塞 I/O(blocking I/O)
- 非阻塞 I/O (non-blocking I/O)
- I/O 多路复用(I/O multiplexing)
- 信号驱动 I/O (signal driven I/O)
- 异步 I/O (asynchronous I/O)
阻塞 I/O 模型
最传统的一种 I/O 模型,用户进程在 I/O 操作的两个阶段都会被阻塞。
那么用户进程发起 I/O 请求之后都经历了什么:
- 第一阶段:
- 当用户进程发出 I/O 请求后,内核会查看内核缓冲区是否有数据。如果内核缓冲区没有数据,则用户进程会进入阻塞状态,并交出CPU。
- 当数据准备成功或者出错后,用户进程才结束本次的阻塞。
- 第二阶段:
- 当数据到达内核缓冲区后,用户进程会继续调用内核提供的系统调用完成数据从内核缓冲区到进程的拷贝,这个过程用户进程仍旧被阻塞。
- 虽然从内核缓冲区拷贝数据到进程的操作由内核完成,但是触发这一个操作的是进程,是进程发起的,所以由第二阶段定义阻塞 I/O 也是同步 I/O。

所以,阻塞 I/O 的一个特点就是在 I/O 操作的两个阶段上都自身都被阻塞。
非阻塞 I/O 进程
和阻塞 I/O 相比,非阻塞 I/O 模型中的用户进程在 I/O 操作的第一阶段不发生阻塞。为什么?当然是实现机制的区别。
在非阻塞 I/O 模型中,用户进程发起 I/O 请求之后经历了什么:
- 第一阶段:
- 当用户进程调用系统接口读取数据时,如果数据没有准备好,则内核会返回一个标志来标识这种情况,此时用户进程不再进行阻塞。
- 如果不阻塞,当内核准备好数据时用户进程如何感知呢?在非阻塞 I/O 模型中,用户进程会通过轮询的方式每隔一段时间就调用一次系统接口,这样当数据准备好后,用户进程就可以进入第二阶段了。
- 使用轮询的方式,在两次轮询的间隙,用户进程可以做其他事情。
- 第二阶段:
- 和阻塞 I/O 完全一样。

非阻塞进程也不是一点问题没有,因为用户进程通过轮询的方式检查数据是否准备好,那么用户进程对CPU的占用率会很高(不交出CPU,一直占用),会影响到系统的整体性能。因此一般情况下很少用这种实现方式。
所以,nonblocking IO的特点是用户进程需要不断地主动询问kernel数据好了没有。
I/O 多路复用模型
前面说过,非阻塞 I/O 模型有一个特点就是用户进程需要不断地主动轮询kernel数据是否准备好,这样的操作导致CPU的占用率很高。如何规避这一个问题呢?内核给出了一个解决办法,就是 I/O 多路复用。
但是你会发现,内核也是通过轮询的方式监控 I/O 通道接口是否准备好数据,那么和用户轮询有什么区别呢?
- 内核不再只监控一个 I/O 通道,而是多个。和内核相比,用户进程只需要关注自己想要读写数据的 I/O 通道即可,至于系统中其他的 I/O 通道则不关心。
- 将轮询转移到内核能减少用户进程对CPU的占用率。
内核如何实现 I/O 多路复用
- select
- Linux提供了select,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select可以帮我们侦测多个fd是否处于就绪状态。
- 时间复杂度 O(n)。因为 select是顺序扫描fd是否就绪,只能无差别轮询所有流,而且支持的fd数量有限(1024个),因此它的使用受到了一些制约。
- poll
- 时间复杂度同样是 O(n)
- 和 select 的区别是它轮询 I/O 通道的数量没有限制。
- epoll
- 时间复杂度O(1)。epoll不再通过轮询的方式检查每个I/O通道是否已经准备好数据,而是通过事件的方式,也就是每个I/O通道准备好数据之后会通过回调函数通知epoll数据已经准备好,这样epoll的时间复杂度就是O(1),而不再是O(n)。然后epoll再通知用户进程数据已经准备好了。
- 想要epoll帮助用户进程监控I/O通道,那么用户进程需要到epoll上进行注册。

为什么要让 select、poll 或者 epoll 监控多个 I/O 通道呢?
- 因为这样设计有利于当有多个I/O流同时请求且需要监控时,内核不需要启动多个进程,而只需要单个process就可以实现监控所有的I/O通道。当某个socket有数据到达时,内核就通知用户进程。
- 如果连接数不是很高,那么 I/O 多路复用模型的效率就不如mutil-threading + blocking IO的性能好,延迟会大一些。因为 I/O 多路复用模型经过两层系统调用,即select和recvfrom。而阻塞 I/O 模型则只调用了 recvfrom。但是当IO通道多的时候,IO复用模型的效率就显示出来了。
mutil-threading + blocking IO
这种多线程结合阻塞式模型的一个非常大的不同就是它并不需要去轮询IO通道,而是通过一个线程执行一次系统调用来执行IO系统操作,这样就不会占用大量CPU的时间,但是维护多线程环境则会占用较多资源,并给编程带来一些挑战。
I/O 复用模型和阻塞 I/O模型以及非阻塞 I/O模型对比:
- 和阻塞 I/O 模型对比,其实就是多了select选择器代理,如果当IO通道只有一个的时候,IO复用模型的效率相对于阻塞模型可能会更差一些。
- 和非阻塞 I/O 模型对比,将轮询这个操作由内核完成,那么能减少用户进程对CPU的占用率。
- IO复用模型是阻塞于select/poll系统调用,而阻塞式模型则是阻塞于直接IO系统调用。
信号驱动 I/O 模型
首先说明信号驱动I/O模型是确确实实的非阻塞I/O模型。
前面的非阻塞I/O模型使得用户进程不阻塞的实现方法是通过轮询的方式看内核是否准备好数据,但是在信号驱动 I/O模型中,使用信号机制实现,而不再轮询。这使得阶段一用户进程等待数据的时候确实不阻塞了。
那么信号驱动 I/O 模型如何保证用户进程不阻塞呢?
- 第一阶段:应用进程是调用操作系统内核提供的signal信号处理接口,但是该接口不会造成阻塞而是立即返回。当数据准备好了之后内核则再返回一个信号,告诉应用程序。
- 第二阶段:和前面三种模型完全一样,应用进程仍然会阻塞知道数据复制完毕。

异步 I/O
异步 I/O 真的是千唤万唤始出来。其实说完信号驱动 I/O 模型,异步 I/O 模型就已经呼之欲出了。

前四种模型的同质是第二阶段,在内核将数据准备好之后,通知用户进程调用系统的recvfrom方法完成数据从内核缓冲区到进程的拷贝,这时用户进程仍旧被阻塞。因为数据的拷贝是由用户进程完成,所以前四种 I/O 模型都是同步 I/O。
但是如果数据的拷贝工作由内核完成,那么这就是一个异步 I/O。顺着信号驱动模IO模型,将信号通知的时机放到数据复制完成之后,就是异步IO模型,这样从整体上来看,应用进程从来没有阻塞过,而是一直运行,直到被通知数据已经被复制到自己的空间中了。
这种模型与信号驱动模型的主要区别是:信号驱动模型由内核通知我们何时可以开始一个IO操作;异步IO模型由内核通知我们IO操作何时完成。
总结
同步 I/O 模型
- 阻塞 I/O 模型
- 非阻塞 I/O 模型
- I/O 多路复用模型
- 信号驱动 I/O 模型
都是同步模型,它们的主要区别在第一阶段,每个模型中应用进程阻塞的实现和方式不同,而在第二个阶段则全部相同,都会阻塞于内核复制数据过程。所以不管阻塞和还是不阻塞都是同步模型。它们的区别是在准备数据的过程中,应用进程是不是阻塞。

浙公网安备 33010602011771号