【操作系统】IO 模型

290 阅读5分钟

IO 操作,即输入输出,指的是将数据由程序进程的内存区域输出到其他设备或者由其他设备读取数据输入到进程的内存区域。

阻塞 IO

阻塞 IO(Blocking IO)是最传统的 IO 模型,即在读写数据时会发生阻塞,这个阻塞发生在两个阶段,一个是等待设备可操作,一个是进行操作并等待操作完成。例如在向 Socket 输出数据时,程序会等待网卡直到网卡可写,在网卡可写时将数据从用户内存写入网卡缓冲区。或者是读取数据时,程序需要等待网卡接收缓存中数据到达,在数据到达时将数据读取至用户内存。

这就像我们点外卖后就跑到小区门口去等,等外卖到了我们再拿回家。这个过程中我们什么都没做,浪费了很多时间。

基于这种模型的程序通常会为每个 Socket 建立一条线程,因为进行 IO 系统调用会导致整个线程阻塞,如果程序线程都有 Socket 需要处理,则全部都将阻塞。

Java IO 包所采用的模型就是阻塞 IO 模型,即 Blocking IO,因此 Java IO 包被称为 BIO

非阻塞 IO

当程序使用非阻塞 IO(Non-blocking IO)进行 IO 操作时,不需要等待,调用会直接返回执行结果,如果设备不可用会直接返回 error。因此我们可以通过轮询的方式不断地进行 IO 操作,直到设备准备好了,我们会获取 success 返回值,此时我们再将数据输入到用户内存或者输出到设备。这个过程中等待设备可用的这个阶段是非阻塞的,我们可以有间隔的轮询,在间隔期间我们可以进行其他操作。

这就像我们点外卖后,每隔几分钟我们就打开软件看外卖到了没,如果显示到了我们就去小区门口取外卖。每一次查询到外卖没到,我们都可以做其他事情,然后隔一会再查。

这种模式依赖于内核提供的包装器来进行,类似于 select 系统调用,可以用来判断执行某个系统调用是否会阻塞。如今基本所有操作系统都支持包装器。

多路复用 IO

多路复用 IO(IO Multiplexing)中,会有一个线程专门不断去轮询多个 Socket 的状态,只有当 Socket 真正有读写事件时,才真正调用 IO 读写操作。在此模型中,只需要一个线程就可以管理多个 Socket,系统不需要为每一个 Socket 建立一个线程,也不需要去维护这些线程。

多路复用模型比非阻塞模型效率高是因为在非阻塞模型中,由用户线程自行进行轮询操作判断 socket 的状态,而多路复用模型使用的是额外的线程。

这就像我们点外卖后,如果外卖小哥到了,小区保安就会提醒外卖所属的用户下来取外卖。在这个过程中,我们可以选择去做别的事。

Java NIO 包所采用的模型就是多路复用 IO 模型,是非阻塞 IO 模型的一种,因此叫 Non-Blocking IO(NIO),也叫 New IO。

信号驱动 IO

信号驱动 IO(Signal-driven IO)中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。

这就像我们在点外卖时,会留下我们的手机号,外卖到了的时候,外卖小哥会自己打电话给我们让我们去取外卖。

多路复用模型与信号驱动模型的区别在于,多路复用模型可以由内核实现,如果内核不支持,也可以由我们用户程序自己实现。而信号驱动模型必须由内核支持

异步 IO

异步 IO(Asynchronous IO,AIO)中,当线程发起 IO 操作之后,就立即去做其他事情。而内核在接收到一个异步 IO 请求之后,会立即返回,表示请求成功发起。在数据准备完成之后,内核会主动将数据拷贝到用户内存。拷贝完成之后,内核会向用户线程发送一个信号,表示 IO 操作已完成,此时用户线程可以直接操作数据。

这就像我们点外卖后,外卖小哥到了小区门口之后,小区内部也有一个外卖员,将外卖送到我们家门口,然后敲门让我们拿外卖。这个过程我们不仅不需要干等,连拿外卖的功夫都省了。

异步 IO 与上面四个 IO 模型的区别在于,上面的四个 IO 在实际读写 IO 设备的数据时用户线程都会发生阻塞,虽然这个速度非常快。但是异步 IO 实际读写数据这一步也不会阻塞。因此上面四个 IO 模型都是同步 IO。

异步 IO 收到信号就表示 IO 完成,用户线程只关心数据是否可用,不关心整个 IO 过程是怎么进行的。显然,异步 IO 模型肯定需要内核的支持。

图像演示

书里的这张图描述得非常准确:

IO 模型比较