四种主要的 IO 模型
四种主要的 IO 模型
1.内核缓冲区和进程缓冲区
说到 IO 模型的时候,我们先说说绕不开的 内存缓冲区。缓冲区的目的,是为了减少频繁地与设备之间的物理交换,我们知道,外部设备的直接读写,会涉及到操作系统的中断,发生和结束系统中断的时候,需要保存和恢复进程数据等信息,为了减少这种底层系统的时间损耗和性能损耗,于是出现了内存缓冲区。
在 linux 操作系统中,操作系统内核只有一个内核缓冲区,但是每个用户进程都有自己的缓冲区,叫做进程缓冲区。有了这两个概念,再来说下 read 和 write 操作。
read 系统调用。并不是直接从物理设备把数据读取到内存中,是把数据从内核缓冲区复制到进程缓冲区。
write 系统调用。也不是直接把数据写入到物理设备,是把数据从进程缓冲区复制到内核缓冲区。
所以,上层程序的 IO 操作,实际上不是物理设备级别的读写,而是缓存的复制。数据在内核缓冲区和物理设备之间的交换,是由操作系统内核来完成的。
2.阻塞和非阻塞
阻塞是指用户线程一直在等待,直到内核 IO 操作彻底完成,在这期间不能干别的事情。
非阻塞是指用户线程拿到内核返回的状态值就返回自己的空间,干别的事情去了。
3.同步和异步
同步是指用户空间的线程是主动发起请求的一方,内核空间是被动接受方。
异步则刚好相反,指操作系统内核是主动发起请求的一方,而用户线程是被动接收方。
根据以上概念,我们将 IO 模型分为四种:同步阻塞 IO,同步非阻塞 IO,IO 多路复用,异步 IO。
4.四种 IO 模型
4.1 同步阻塞 IO
在 java 应用进程中,默认情况下,所有的 socket 连接的 IO 操作都是同步阻塞 IO,java 进程从 IO 系统调用开始,直到系统调用返回,这段时间内,java 进程是阻塞的。
4.1.1举个栗子
从 java 进程启动 IO 的 read 调用开始,用户线程进入阻塞状态;
系统内核收到调用开始准备数据,这时候,数据还没有到达内核缓冲区,这时候内核就要等待;
内核一直等到完整的数据后,就会将数据从内核缓冲区复制到用户的进程缓冲区,然后等内核返回结果;
当内核返回后,用户线程才会解除阻塞状态,重新开始运行。
4.1.2 阻塞 IO 的优缺点
通过以上的栗子,我们来说说阻塞 IO 的优缺点:
优点:用户程序开发简单,在阻塞等待数据期间,用户线程挂起,这段时间内,用户线程基本不会占用 CPU 资源。
缺点:通常,会为每个连接配备一个独立的线程,由这个线程来维护一个连接的 IO 操作。那么在高并发的情况下,就需要大量的线程来维护大量的网络连接,内存和线程切换的开销将会非常巨大。
4.2 同步非阻塞 NIO
socket 连接默认是阻塞模式,在 Linux 系统中,可以通过设置将 socket 便成为非阻塞的模式。使用非阻塞模式的 IO 读写,叫做同步非阻塞 IO,简称为 NIO 模式。
4.2.1举个栗子
发起一个非阻塞 socket 的 read 读操作的系统调用,在内核数据没有准备好的情况下,用户线程发起 IO 请求时,立即返回。为了读取到最终的数据,用户线程需要不断地发起 IO 系统调用;
内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到进程缓冲区,然后返回结果;
用户线程读取到数据后,才会接触阻塞状态,重新运行。所以,用户进程需要经过多次尝试,才能保证最终真正读取到数据,然后继续执行。
应用程序的线程需要不断地进行 IO 系统调用,轮询数据是否已经准备好了,如果没有准备好,就继续轮询,直到完成 IO 系统调用为止。
4.2.2 同步非阻塞 IO 的优缺点
优点:每次发起 IO 系统调用,在内核等待数据过程中可以立即返回,用户线程不会阻塞 ,实时性较好。
缺点:不断轮询内核,将会占用大量的 CPU 时间,浪费性能,效率低下,因此,在高并发的情况下,同步非阻塞模型也是不可用的。
4.2.3 说明
同步非阻塞 IO,可以简称 NIO,但是,它不是 java 中的 NIO,java 中的 NIO(New IO),是另外一种模型,叫做 IO 多路复用模型。
4.3 IO 多路复用模型
为了避免在同步非阻塞 IO 模型中轮询等待的问题,在 IO 多路复用模型中,引入了一种新的系统调用,查询 IO 的就绪状态。在 Linux 系统中,对应的系统调用为 select/epoll 系统调用,通过该系统调用,一个进程可以监视多个文件描述符,一旦某个文件描述符就绪(一般指的是内核缓冲区可读/可写),内核就能将就绪状态返回给应用程序。
select 系统调用,几乎所有的操作系统都支持,具有良好的跨平台特性。
epoll 是在 Linux2.6 内核中提出的,是 select 系统调用的 Linux 增强版本。
在 IO 多路复用模型中通过 select/epoll 系统调用,单个应用程序的线程,可以不断地轮询成百上千的 socket 连接,当某个或者某些 socket 网络连接有 IO 就绪的状态,就返回对应的可以执行的读写操作。
4.3.1举个栗子
首先是选择器注册。将需要 read 操作的目标 socket 网络连接,提前注册到 select/epoll 选择器中,然后才可以开启整个 IO 多路复用模型的轮询;
通过选择器的查询方法,查询所有注册过的 socket 连接的就绪状态,通过该查询,内核返回一个就绪的 socket 列表。当任何一个 socket 中的数据准备好了,内核缓冲区就有数据了,内核就将该 socket 加入到就绪的列表中。注:当用户进程调用了 select 查询方法,那么整个线程就会被阻塞。
用户线程获取了就绪的 socket 列表后,根据其中的 socket 连接发起 read 系统调用,用户线程阻塞,内核开始将数据从内核缓冲区复制到用户缓冲区。
复制完成后,内核返回结果,用户线程解除阻塞状态,用户线程读到数据,继续执行。
4.3.2 特点
和 NIO 模型相似,多路复用 IO 模型也需要轮询。负责 select/epoll 状态查询调用的线程,需要不断轮询,查找出 IO 操作就绪的 socket 连接。对于注册在选择器上的每一个可以查询的 socket 连接,一般都设置为同步非阻塞模型,这一点,用户程序是无感知的。
4.3.3 优缺点
优点:与一个线程维护一个连接的阻塞 IO 相比,使用 select/epoll 的好处,就是一个选择器线程可以同时处理成千上万个连接,系统不需要创建大量线程,大大减小了开销,java 语言的 NIO(New IO),使用的就是 IO 多路复用,在 linux 系统上,使用的是 epoll 系统调用。
缺点:本质上,select/epoll 系统调用时阻塞的,属于同步 IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,这个读写过程是阻塞的。
要想彻底解除线程阻塞,就需要异步 IO 模型。
4.4 异步 IO 模型 (AIO)
4.4.1 AIO 基本流程
用户线程通过系统调用,向内核注册某个 IO 操作,内核在整个 IO 操作完成后,通知用户程序,用户执行后续的操作。在整个异步 IO 模型中,包括内核将数据从网卡读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不阻塞。
4.4.2 举个栗子
用户线程发起 read 调用后,立即可以做其他的了,用户线程不阻塞;
内核开始准备数据,等数据准备好了,内核就将数据从内核缓冲区复制到用户缓冲区;
完成后,内核会给用户线程发送一个信号,或者调用户线程注册的回调接口,通知用户线程 read 操作已完成;
用户线程读取数据,继续后续操作。
4.4.3 优缺点
优点:异步 IO 操作,在内核等待数据和复制数据的两个阶段,用户线程都是不阻塞的。异步 IO 是真正的异步输入输出,它的吞吐量高于 IO 多路复用模型。
缺点:应用程序仅需要进行事件的注册和接收,其余的工作都交给了操作系统,所以需要底层的内核提供支持。
在 Linux 系统下,异步 Io 模型在 2.6 版本才开始引入,目前还不完善,底层实现仍使用 epoll,与 IO 多路复用相同,在性能上没有明显优势。Netty 框架,使用的就是 IO 多路复用模型,而不是异步 IO 模型。
版权声明: 本文为 InfoQ 作者【方明】的原创文章。
原文链接:【http://xie.infoq.cn/article/78c92db1297d62a7ed1eb7284】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论 (2 条评论)