理解BIO、NIO、AIO

简介: 理解BIO、NIO、AIO

一、I/O概述

I/O(input/output)广义上包括I/O设备和I/O接口。I/O设备包括打印机、键盘、鼠标、写字板、麦克风、音响、显示器等等,而I/O接口是计算机主机与外部设备进行数据交互的通道。

实际上,我们编写的程序以及在计算机系统上的操作都离不开与I/O之间的交互,这些操作一般称为I/O操作。

由于应用程序大多数工作于Linux操作系统下,我们以下将围绕着Linux/Unix系统展开,但有些原理在Windows下仍然是成立的。

在Linux操作系统下,兼容POSIX标准,“一切皆文件”,所有对于I/O的操作都是通过文件展开,而对于文件的读写是通过内核维护的文件描述符(file   descriptor,fd)进行的。当程序打开一个现存的文件或者新建一个文件时,内核会返回一个fd,读写时也需要使用一个fd来指定一个明确的文件。fd是一个非负的整数,例如我们常见的标准输入对应的是0,标准输出对应的是1,标准错误输出对应的是2。不同的fd可能指向同一个文件,相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开,操作系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。

以一次read操作为例,涉及的交互流程如下:

1、应用程序通过read调用(system call)向内核发起读请求。

2、内核通过磁盘控制器向硬件发起读指令以从磁盘读取数据,等待读就绪。

3、磁盘控制器通过DMA将数据直接写入内核内存缓冲区

4、磁盘控制器完成内核缓冲区的填充后,读就绪,内核将数据从内核空间中的临时缓冲区复制到进程指定的缓冲区中。

从以上的流程可以看到一次操作涉及到了用户进程和内核进程,涉及到了数据从内核缓冲区到进程缓冲区的复制。不同的I/O方式使得进程的状态、处理流程也不一致,因此区分出了不同的I/O模型。

二、I/O模型

我们将I/O分为磁盘I/O和网络I/O,磁盘I/O一般指普通的文件读写,网络I/O特指网络通信中的socket通信涉及的I/O,磁盘I/O对应的底层设备是磁盘,网络I/O对应的设备是网卡,工作流程较为相似。

在Linux操作系统下根据不同的I/O方式分为同步/异步I/O、阻塞/非阻塞I/O、异步I/O(Asynchronous  I/O),其中前两种组合以后又有同步阻塞I/O(Blocking I/O)、同步非阻塞I/O(Non-Blocking  I/O),甚至在一些场景下还会有DIO(Direct I/O)、Memory-Mapped  I/O(内存映射I/O),DIO和内存Memory-Mapped I/O不常用,在这里我们暂不做讨论。

1、同步/异步(Synchronous/Asynchronous)

同步和异步关注的是消息通信时的机制。同步是进程/线程主动等待调用的结果,在结果没有得到结果之前,调用不返回,这通常是一个比较可靠的机制,虽然性能上会受到一些影响;异步则相反,不管是否得到结果,在发起调用以后就立即返回,依靠事件通知、函数回调等机制来获取调用的结果。

2、阻塞/非阻塞(Blocking/Non-Blocking)

阻塞和非阻塞关注的是进程/线程在等待调用结果时的状态。阻塞的I/O模式下,进程/线程在发起读请求以后将会一直处于等待状态直到数据从内核缓冲区拷贝到用户进程缓冲区,而非阻塞I/O模式下,进程/线程发起请求后不等待数据从内核缓冲区拷贝到用户进程缓冲区就直接返回了,进程/线程本质上是不阻塞的。

关于同步阻塞I/O、同步非阻塞I/O经常使人疑惑,那是因为不管是阻塞还是非阻塞I/O,由于消息通信是同步的,在宏观上都需要等待最终结果的返回,这是直观呈现出来的,经常是被封装了的,应用程序开发者“看起来”就像是同步意味着阻塞,这是视角的不同。

接下来,我们对最常见的同步阻塞I/O(Blocking  I/O)、同步非阻塞I/O(Non-Blocking I/O)、I/O多路复用模型(I/O Multiplexing)、信号驱动I/O(SIG  IO)、异步I/O(Asynchronous  I/O)这几种I/O模型进行深入一些的理解,可见的我们讨论的这些I/O模型在网络编程中的应用广泛,我们接下来将以Java体系下socket编程为背景进行介绍。

1、同步阻塞I/O(Blocking I/O)

默认情况下,socket连接的IO操作是同步阻塞I/O,应用程序从I/O系统调用开始,直到系统调用返回,在这段时间内,Java进程是阻塞的。返回成功后,应用进程开始处理用户空间的缓存区数据。

A、java程序进行read系统调用,线程进入阻塞等待返回状态;

B、内核收到read系统调用,等待socket数据包填充到内核缓冲区;

C、内核等到数据填充好了内核缓冲区以后,将数据从内核缓冲区复制到用户线程的缓冲区,返回read系统调用的结果。

D、用户线程解除阻塞状态,从用户线程缓冲区中获取数据。

在同步阻塞I/O模式下,为每个连接配置一个独立的线程,一个线程维护一个连接的I/O操作,模型简单,但在高并发的场景下,需要大量的线程来维护网络连接,对于内存的消耗、线程上下文切换的开销都比较大,因此BIO模型无法适应高并发的场景。

2、同步非阻塞I/O(Non-Blocking I/O)

我们同样以recvfrom的系统调用为例来理解一下同步非阻塞I/O的流程:

A、java程序进行recvfrom系统调用,此时数据还未到达内核缓冲区,不阻塞直接返回,为了读取到最终的数据,用户线程继续发起recvfrom系统调用;

B、重复A直到数据到达内核缓冲区,此时用户线程阻塞,等待内核将数据从内核缓冲区复制到用户线程缓冲区。

C、内核将数据从内核缓冲区复制到用户线程缓冲区然后返回结果;

D、用户线程读取到数据,解除阻塞状态。

同步非阻塞I/O模型下,每次发起的IO系统调用,在内核等待数据过程中可以立即返回,用户线程不会阻塞,但是应用程序的线程需要不断地进行IO系统调用,轮询内核数据是否已经准备好,直到完成IO系统调用为止,这将占用大量的CPU时间,效率低下。

总体来说,在高并发应用场景下,同步非阻塞IO也是无法适应的,一般Web服务器不使用这种IO模型,在Java网络编程的实际开发中,也不会直接使用这种IO模型。

3、I/O多路复用(I/O Multiplexing)

I/O多路复用模型是为了解决在同步非阻塞I/O模型中出现的轮询发起系统调用以确定数据是否已经准备好导致大量消耗CPU时间的缺点。

I/O多路复用模型,引入了一种新的系统调用来支持查询I/O的就绪状态,也就是上面说的数据是否已经准备好了。这种系统调用有select、poll、epoll等,通过该系统调用,一个进程就可以监视多个文件描述符,一旦某个描述符就绪(内核缓冲区可读/可写),内核就立即将就绪状态返回给应用程序。

在高并发的情况下,实际上会为每个用户建立一个文件描述符socket fd,所以会出现大量的fd需要监控,I/O多路复用恰恰解决了这个难题。

select/poll目前几乎在所有的操作系统上都被支持,但性能较差,Linux2.6内核开始提出了epoll,是性能更好的选择,应用也比较广泛,例如著名的反向代理服务器nginx就使用了epoll。

发起一个多路复用IO的read读操作的系统调用,流程如下:

A、将需要read操作的目标socket网络连接注册到select/epoll选择器中,开启IO多路复用模型的轮询流程,在Java中对应的选择器类是Selector类。

B、调用选择器的查询方法,此时线程阻塞直到任何一个注册过的socket中的数据准备好了,内核缓冲区有数据(就绪)了,内核就将该socket加入到就绪的列表中并返回给用户线程。

C、用户线程获得就绪状态的列表后,根据其中的socket连接,发起read系统调用,此时用户线程阻塞,内核开始复制数据,将

数据从内核缓冲区复制到用户缓冲区,直到复制完成后返回。

D、用户线程从缓冲区中读取到数据,线程解除阻塞状态。

与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接,系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。IO多路复用也需要不断轮训是否有IO就绪,但是这个操作是内核完成的,用户不需要不断地进行轮询白白消耗CPU时间,这是其强于标准NIO模型的原因。

select/poll和epoll在性能上有显著的差别,epoll的性能优势明显。select和poll的实现方式类似,他们通过一个select或poll函数阻塞式的等待着数据就绪,当调用这个函数时,会将fd集合从用户态拷贝到内核态,并对所有的fd进行遍历以判断哪个fd发生就绪事件,并且select只支持最大1024个文件描述符。epoll提供了epoll_create、epoll_ctl、epoll_wait三个函数,epoll创建epoll句柄,epoll_ctl则是注册要监听的事件类型,在新的注册时会将fd拷贝进内核,而不是类似select每次调用都拷贝,这样子在fd数量很多的时候就相对少了部分拷贝的时间,epoll维护了等待就绪队列,只需要定时查看就绪队列中有没有存在就绪的fd即可,而不需要对所有的fd都进行遍历,因此更加高效。

另外,epoll能支持的fd数量上限是系统可以打开的文件的数目,远大于1024,具体数目可以查看/proc/sys/fs/file-max配置。

总结地说,select/epoll系统调用是阻塞式的,所以I/O多路复用模型本质上还是阻塞I/O,但是由于只需要少量线程就可以同时处理大量的连接,所以这种模型已经能够比较好的支持高并发的场景。

4、信号驱动I/O(SIG I/O)

信号驱动I/O模型增加了一个新的系统调用sigaction,这是一个非阻塞的系统调用,其目的是提供注册信号处理函数的功能,避免了select/epoll在等待就绪列表时阻塞的问题。

当数据报准备好时,内核为该进程生成一个SIGIO信号,在信号处理程序中调用就可以调用recvfrom来读数据报,这样子的话在读取数据报的阶段是阻塞的,但是在等待就绪列表的过程是不阻塞的。流程描述如下:

A、编写信号处理程序,并调用sigaction系统调用,此时内核立即返回。

B、内核缓冲区数据准备好(就绪)向线程发送SIGIO信号。

C、信号处理程序接受到SIGIO信号,调用recvfrom系统调用,此时线程阻塞。

D、数据从内核缓冲区复制到用户缓冲区,线程读取到数据,解除阻塞状态。

5、异步I/O(Asynchronous I/O)

异步I/O简称AIO,是完全非阻塞式的设计。首先,用户线程通过系统调用,向内核注册某个IO操作,然后当次调用立刻返回,无需等待。当内核在整个IO操作(包括数据准备、数据复制)完成后,会主动通知用户程序,由用户程序执行后续的操作。相较于I/O多路复用,异步I/O无需经历select/epoll阶段的阻塞。流程描述如下:

A、编写信号处理程序,并调用aio_read系统调用,此时内核立即返回。

B、开始准备数据,直到准备好,内核会将数据从内核缓冲区复制到用户缓冲区

C、完成数据拷贝后,内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。

D、用户线程读取用户缓冲区的数据,完成后续的业务操作。

从以上流程可知,在内核等待数据和复制数据的两个阶段,用户线程都是非阻塞的,用户线程只需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数,其余的工作都留给了操作系统,换句话说,这需要底层内核提供支持。  理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。

三、AIO的演进

虽然AIO从理论上来说性能是最好的,但是其应用却并不广泛,反而是I/O多路复用模型应用广泛,这有其历史原因。

异步I/O的实现有赖于操作系统内核的支持,在Windows系统下提供了一个叫做  I/O Completion Ports 的方案,通常简称为IOCP,操作系统负责管理线程池,其性能非常优异,所以在 Windows 中  JDK 直接采用了 IOCP  的支持,通过IOCP实现了真正的异步IO;而在Linux系统下,异步I/O的模型在内核2.6版本才引入,而且当时的底层实现仍使用epoll,在性能上没有明显的优势,因此JDK  没有直接采用Linux的AIO,而是采用了自建线程池的方式,其实现也未尽如人意。

大多数的高并发服务器端的程序,一般都是基于Linux系统的,加之有netty等优秀的开源网络应用程序开发框架,更加推动了I/O多路复用模型的广泛应用,相较之下AIO缺少更多应用场景的考验,应用缓慢。

相关文章
|
1月前
|
网络协议 Dubbo Java
一文搞懂NIO、AIO、BIO的核心区别(建议收藏)
本文详细解析了NIO、AIO、BIO的核心区别,NIO的三个核心概念,以及NIO在Java框架中的应用等。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
一文搞懂NIO、AIO、BIO的核心区别(建议收藏)
|
20天前
|
Java
BIO、NIO、AIO 有什么区别
BIO(阻塞I/O)模型中,服务器实现模式为一个连接一个线程;NIO(非阻塞I/O)使用单线程或少量线程处理多个请求;AIO(异步I/O)则是在NIO基础上进一步优化,采用事件通知机制,提高并发处理能力。
43 5
|
20天前
|
消息中间件 监控 Java
BIO、NIO、AIO在不同场景下的应用对比
BIO(阻塞I/O)、NIO(非阻塞I/O)和AIO(异步I/O)是Java中处理I/O操作的三种模式。BIO适用于连接数少且稳定的场景;NIO通过非阻塞模式提高并发处理能力,适合高并发场景;AIO则完全异步,适合需要高效、低延迟的I/O操作场景。
68 4
|
3月前
|
Java
Netty BIO/NIO/AIO介绍
Netty BIO/NIO/AIO介绍
|
2月前
|
Java Linux 应用服务中间件
【编程进阶知识】高并发场景下Bio与Nio的比较及原理示意图
本文介绍了在Linux系统上使用Tomcat部署Java应用程序时,BIO(阻塞I/O)和NIO(非阻塞I/O)在网络编程中的实现和性能差异。BIO采用传统的线程模型,每个连接请求都会创建一个新线程进行处理,导致在高并发场景下存在严重的性能瓶颈,如阻塞等待和线程创建开销大等问题。而NIO则通过事件驱动机制,利用事件注册、事件轮询器和事件通知,实现了更高效的连接管理和数据传输,避免了阻塞和多级数据复制,显著提升了系统的并发处理能力。
69 0
|
4月前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
93 2
|
5月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
5月前
|
Java
Java中的NIO编程详解
Java中的NIO编程详解
|
5月前
|
Java 大数据
如何在Java中进行网络编程:Socket与NIO
如何在Java中进行网络编程:Socket与NIO
|
5月前
|
Java
Java中的NIO编程详解
Java中的NIO编程详解