IO 模型

本贴最后更新于 1664 天前,其中的信息可能已经时移世改

简介

当用户进程发出一次读数据请求时,数据并非直接从网络或者磁盘到达用户空间,而是经过了系统内核的中转,即数据先由网络或者磁盘到达系统内核空间(数据等待阶段),然后再从系统内核空间复制到用户空间(数据复制阶段)。这两阶段中,为了更好的协调 CPU 和外设之间的工作,逐渐发展出了各种 IO 模型。

IO 模型的分类

在《UNIX 网络编程》中,将 IO 模型一共分为五种:阻塞式、非阻塞式、IO 多路复用、信号驱动和异步。接下来,我们将逐个介绍。

阻塞式 IO

WX201909291931162x.png
在阻塞式 IO 中,一个读操作的流程往往如图所示。我们可以看到,用户进程在进行 recv 系统调用后会被阻塞,直到数据已经被复制到了用户空间。几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv() 等接口开始的,这些接口都是阻塞型的。
如果用单进程/单线程去处理所有请求的话,则线程/进程很有可能被阻塞在了对某个网络连接的 IO 操作中,从而来不及处理另外的网络连接。因此后端业务系统往往在每个网络连接创建时给该连接创建一个单独的线程。在小并发量的情况下,这种一个连接对应一个线程的模式,确实可以很好的处理网络 IO 操作。然而,由于每个线程都会占用一部分内存,一旦系统并发量变大,一个连接对应一个线程的模式可能会导致系统内存不足;另外,大并发量情况下,一个连接对应一个线程的模式会频繁的创建线程和频繁的线程调度,都会严重的拖累系统性能,从而导致系统不可用。
总的来看,阻塞式 IO 只适用于小并发量的系统,不适合海量并发的场景。

非阻塞式 IO

WX201909291948142x.png
同样以读操作为例,非阻塞式 IO 的流程如图所示。非阻塞式 IO 和阻塞式 IO 的不同,主要体现在数据等待阶段:非阻塞式 IO 中,用户进程进行了读操作的系统调用后,如果内核中的数据没有准备好的话,系统不会将用户进程阻塞,而是直接返回一个表示数据未准备好的状态码。从用户进程来看,进程发出一个读请求后并不需要等待 ,而是立马得到一个结果。用户进程通过该结果即可知道数据并没有准备好,从而后续会再次发起读请求。一旦系统内核中的数据准备好,用户进程再次发起读请求时,系统会将用户进程阻塞,直到数据从内核复制到用户空间完毕。
我们不难看出,在非阻塞式 IO 模型情况下,为了读取到数据,用户进程需要主动地不停的发起读请求系统调用,从而一直处于忙等待的状态。

IO 多路复用

WX201909292010172x.png
从 IO 多路复用的流程中可知,与阻塞式 IO 类似,用户进程会阻塞在数据准备和数据复制两个环节。但与阻塞式 IO 不同,在 IO 多路复用中,一个用户进程通过 select、poll、epoll 系统调用,可以监视多个网络连接,一旦某个或某几个连接可读(数据已经到达系统内核)时,select、poll、epoll 系统调用会立即返回。用户可以通过 select、poll、epoll 系统调用的参数获取到可读、可写的连接的文件描述符。通过连接的文件描述符,即从连接读取数据或者写入数据。
利用 IO 多路复用的特性,我们可以用 java 的 NIO 构建 Reactor 模式的应用程序。这部分内容可以参考《 从 Java NIO 到 Reactor 模式

信号驱动 IO

WX201909292023092x.png
信号驱动式 IO 就是指进程预先向内核注册一个信号处理函数,然后用户进程返回不阻塞。当内核数据就绪时会发送一个信号给进程,用户进程便在信号处理函数中调用 IO 读取数据。图中可以看出,数据从内核拷贝到用户进程的过程中,用户进程还是会阻塞的。

异步 IO

image.png
在异步 IO 中,用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而从 kernel 的角度,当它收到一个 read 请求后,会立刻返回,不会对用户进程产生任何 block。同时,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。
下图是基于异步 IO 构建的 Proactor 模式的应用示意图。在 Proactor 中,有一个 Initiator 模块负责初始化系统,一个异步操作处理器(往往由系统内核提供)负责执行异步操作(如读、写)。异步操作完成后,异步操作处理器将完成事件或者失败事件放入完成事件队列。Proactor 模块通过监视器监听完成事件队列并获取到事件,从而调用 Completion Handler 进行后续的处理。
image.png

同步/异步、阻塞/非阻塞的区别

很多情况下,人们经常会把同步/异步 IO 的概念与阻塞/非阻塞 IO 的概念混同,即简单的将同步 IO 等同于阻塞 IO,将异步 IO 等同于非阻塞 IO。其实本质上这两种概念还是有区别的。简单来讲,阻塞/非阻塞指的是进程发起操作时,进程本身所处的状态;而同步/异步则指的是操作完成的通知机制。比如在读操作中,如果是用户进程主动的去了解到数据已被读取,则为同步读;如果是由系统内核通过回调或者通知的方式告知用户进程操作已完成,则属于异步读。由此可见,同步/异步、阻塞/非阻塞分别描述的是两个不同维度的事情。

总结

本文主要介绍了阻塞式、非阻塞式、IO 多路复用、信号驱动式和异步式等五种 IO 模型,并对同步/异步、阻塞/非阻塞的概念做了一个区分。其中,IO 多路复用是 Reactor 模式的基础,而 Reactor 模式被应用到了当前很多开源高性能 IO 组件中(如 Netty、Kafka、Nginx 等)。了解 IO 模型,是构建高性能 IO 组件、开发网络应用的一个充分前提。

  • IO
    8 引用 • 20 回帖
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 440 关注
  • Proactor
    1 引用

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...