文章目录
- 计算机角度IO
- 操作系统IO
- 常见的IO模型
- Java 中 3 种常见 IO 模型
- BIO(BlockingI/O)【同步阻塞IO】
- NIO(Non-blocking/New I/O)【非阻塞IO】
- IO多路复用
- AIO(Asynchronous I/O)【异步IO】
计算机角度IO
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
IO (Input/Output) 通常是指计算机与外部设备之间的数据交换过程。输入设备(如键盘、鼠标、摄像头等)把数据输入到计算机中,输出设备(如显示器、打印机、扬声器等)把数据从计算机中输出。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
我们再先从应用程序的角度来解读一下 I/O。
为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
操作系统IO
从操作系统角度来看,IO是指操作系统通过设备驱动程序与硬件设备进行数据交换的过程。操作系统通过系统调用、中断等机制控制IO操作,进而实现数据的输入和输出。操作系统通过各种设备驱动程序来管理硬件设备,使得应用程序可以方便地对设备进行访问,并获得所需的输入输出数据。
在计算机中,IO是所有操作中最耗时的一部分,因为设备的存储和处理速度比主存储器和CPU低得多。所以,为了缩短IO的响应时间,操作系统通常会使用缓存技术,将提前读取或写出的数据缓存到内存里,加快后续访问的速度。此外,操作系统还会针对不同类型的设备使用不同的IO调度算法,以提高整体IO效率,例如 FCFS(先来先服务)、SJF(最短作业优先)、CFQ(完全公平调度算法)等。
操作系统负责计算机的资源管理和进程的调度,我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等。真正的 IO 是在操作系统执行的。即应用程序的 IO 操作分为两种动作:IO 调用 和 IO 执行。IO 调用是由进程(应用程序的运行态)发起,而 IO 执行是操作系统内核的工作。
应用程序发起的一次 IO 操作包含两个阶段:
IO 调用:应用程序进程向操作系统内核发起调用。
IO 执行:操作系统内核完成 IO 操作。
真正的IO都是操作系统执行的,应用程序IO一般两种:IO调用和IO执行
看似Java在读,实际是操作系统在读
常见的IO模型
UNIX 系统下, IO 模型一共有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
Java 中 3 种常见 IO 模型
BIO(BlockingI/O)【同步阻塞IO】
阻塞IO模型也称为同步IO模型,在这种模型中,一个线程需要在所有IO操作完成之后才能继续执行后续的代码,因此也被称为“同步”模型。在阻塞IO模型中,当一个线程调用了
read()或write()
等IO操作时,线程会一直等待,直到操作系统完成IO操作并返回结果,才会继续执行后续的代码。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
假设应用程序的进程发起 IO 调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次 IO 操作,称之为阻塞 IO。
Java之前所学的IO都是阻塞式IO。
阻塞 IO 的缺点就是:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞IO 优化。
NIO(Non-blocking/New I/O)【非阻塞IO】
非阻塞IO模型也称为异步IO模型,在这种模型中,一个线程可以发起IO请求后立即返回,而不需要等待IO操作的结果,因此也被称为“异步”模型。在非阻塞IO模型中,当一个线程调用了
read()或write()
等IO操作时,线程不会等待操作系统返回结果,而是继续执行后续的代码。当操作系统完成IO操作后,它会通知应用程序,告知IO操作的结果。
如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞 IO
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
这个时候,I/O 多路复用模型 就出现了
IO多路复用
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。发起请求数据时,操作系统立即返回一个结果(不是数据),等数据完全准备好了,向用户进程进行响应。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
既然 NIO 无效的轮询会导致 CPU 资源消耗,我们等到内核数据准备好了,主动通知应用进程再去进行系统调用。
IO 复用模型核心思路:系统给我们提供一类函数(如 select、poll、epoll),它们可以同时监控多个 fd 的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom 系统调用。
文件描述符 fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
select:应用进程通过调用 select 函数,可以同时监控多个 fd,在
select
函数监控的 fd中,只要有任何一个数据状态准备就绪了,select
函数就会返回可读状态,这时应用进程再发起recvfrom()
请求去读取数据。非阻塞 IO 模型(NIO)中,需要 N(N>=1)次轮询系统调用,然而借助
select
的 IO 多路复用模型,只需要发起一次询问就够了,大大优化了性能。缺点:监听的 IO 最大连接数有限,在 Linux 系统上一般为 1024。
select
函数返回后,是通过遍历 fdset,找到就绪的描述符 fd。(仅知道有 I/O 事件发生,却不知是哪几个流,所以遍历所有流). 因为存在连接数限制,所以后来又提出了poll。与 select 相比,poll 解决了连接数限制问题。但是,select 和 poll
一样,还是需要通过遍历文件描述符来获取已经就绪的socket
。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。因此出现了epoll
epoll: 为了解决 select/poll 存在的问题,多路复用模型 epoll 诞生,它采用事件驱动来实现。epoll 先通过
epoll_ctl()
来注册一个 fd(文件描述符),一旦基于某个 fd 就绪时,内核会采用回调机制,迅速激活这个 fd,当进程调用epoll_wait()
时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是 epoll 的亮点。
epoll 明显优化了 IO 的执行效率,但在进程调用
epoll_wait()
时,仍然可能被阻塞。能不能不用我老是去问你数据是否准备就绪,等我发出请求后,你数据准备好了通知我就行了,这就诞生了信号驱动 IO 模型。
IO 模型之信号驱动模型
信号驱动不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号,然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通 过信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用
recvfrom
,去读取数据。
信号驱动 IO 模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是上面的流程图,发现数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,不管是 BIO,还是 NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。
AIO(Asynchronous I/O)【异步IO】
前面讲的 BIO,NIO 和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不算是真正的异步。AIO 实现了 IO 全流程的非阻塞,就是 应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程 IO 操作执行完毕。
大概总结一下是这样