JavaI/O专题


1. 什么是I/O

1.1 从硬件的角度上看IO

I/O(Input/Output):输入/输出,按照冯诺依曼提出的体系结构,计算机可以分为五部分

  • 输入设备
  • 中央控制器
    • 控制器
    • 运算器
  • 存储器
  • 输出设备

那么从这个角度上来说,Input就是输入设备输入信号到计算机中

Outpt就是输出设备输出信号到计算机中

1.2 从应用程序的角度上看IO

为了保证操作系统的稳定性和安全性,程序在执行过程中通常会在两种状态间不断切换,这两种状态就是用户态内核态,相对的,一个进程的地址空间也划分为用户空间内核空间

为什么需要划分用户空间/用户态和内核空间/内核态?

在程序运行过程中,为了避免恶意程序的运行导致操作系统崩溃无法工作,因此针对于一些危险操作,比如申请内存、发出I/O请求等,这些操作对于os来说都有可能导致崩溃,比如说用户程序篡改了内核空间的核心参数,将会导致操作系统不可用,因此提出了内核空间和用户空间,隔离了操作空间,这些操作需要更高的权限执行,发出系统调用后执行相关指令。

因此,如果用户想要进行I/O操作的时候,必须通过系统调用来间接访问内核空间

常见的I/O有:磁盘I/O网络I/O

从应用程序的角度上来看,我们的应用程序对操作系统的内核发起I/O调用(系统调用),是操作系统的内核帮助程序员完成了I/O操作,而I/O的过程是不可干预的,一般来说会经历两个阶段

  • 需要数据的进程等待I/O完成,内核等待I/O准备好数据
  • 内核将数据从内核空间拷贝到用户空间

内核准备好数据包括读取请求和写请求

写请求:等待系统调用的完整请求数据,并写入内核缓冲区

读请求:等待系统调用的完整请求数据,如果请求数据不在内核缓冲区,则将外围设备的数据读入到内核缓冲区

2. 讲讲常见的I/O模型

UNIX系统下,I/O模型一共有五种

同步阻塞I/O、同步非阻塞I/OI/O多路复用、信号驱动I/O、异步I/O

说一下什么是同步,什么是异步

举个例子,假设我们要吃泡面

①我们只有一个人,所以煮泡面的的过程->拿厨具->放调料->等泡面,这个过程由我们自己一手包办,等泡面的这个过程需要3分钟,这时候我们只能拿着秒表在那看,秒表走完了面就泡好了

②假设我们有两个人,所以煮泡面的过程:A要煮泡面,然后跟B说,B就去煮了,然后跟A说等面煮好了我叫你,这时候有两种情况的:

  • A很饿,于是隔一段时间就去问B,面煮好没?这种叫做轮询,这时候A是主动方,B是被动方
  • A很忙,顾不上吃泡面,B煮好了面,就去叫A吃面,这时候A是被动方,B是主动方

第一种情况下,所有的事情一个人执行,属于串行执行,每一个步骤都需要基于上一个步骤的基础上执行。

第二种情况下,事情可以不串行执行。

简单来看,我们可以把熊猫看成是主线程,竹子可以看成是它的一个子线程,同步是指串行执行任务,效率低

异步则是将自己当前一些较耗时的任务交给其他线程,让主线程马上就能返回执行其他任务

  • 阻塞:对于需要的条件不具备的时候会一直等待,直到条件具备的时候才会继续向下执行
  • 非阻塞:对于需要的条件不具备的时候不会等待,而是直接返回等到后期具备条件的时候才会回来

3. 什么是BIO?

同步阻塞I/O模型中,应用程序发起read()指令之后,会一直阻塞,直到内核将数据拷贝到用户空间完成

简单来说,BIO就是阻塞式I/O,发起I/O的进程将会被持续阻塞,直到数据被全部拷贝到用户空间

注意,如果BIO是在网络环境下的,那么BIO会在发送方和接收方都完成I/O后才会解除阻塞

BIO最大的问题就是使得进程被阻塞了,虽然在阻塞的过程中会交出CPU来提高CPU的利用率,但是会导致处理此I/O请求的服务性能低下,当不断有读写请求从外部发来的时候,由于本次的I/O没有完成,就会导致请求积压

一个简单的解决方案是当有一个线程接收了I/O请求后,立即开辟新的线程,新的线程监听读写请求,这样可以显著提升请求的响应能力,但是当系统的请求过多,系统并发度高的时候,就会导致系统频繁陷入内核态创建线程,频繁执行用户态和内核态之间的切换和线程上下文切换,这个开销也是不可忽视的

这也是非常重要的一点,也就是接收方的处理线程和发送方的处理线程是成对出现的。

注意,无论是线程的上下文切换,还是用户态和内核态的切换,它都会涉及到一个现场的概念,所谓现场就是在执行程序的时的寄存器状态、虚拟内存到物理内存的映射表的内容,当执行切换的时候,都会先保存现场,然后切换到对应的状态,当执行完毕后,这个现场又被恢复,主要的开销就是这个所谓的现场的保存和恢复的过程。

4. 什么是NIO?

通过之前的讲述,BIO实际上基于操作系统的中断驱动I/O,它最大的问题就是阻塞式I/O,那么能不能不阻塞呢?

这就是Non-Blocking IO,它本质上就是一个阻止了阻塞的操作,但是这会导致CPU忙等,它在发出I/O请求后,不会阻塞,而是不断轮询内核数据是否准备完毕,这期间将会导致频繁的内核态切换,而且在CPU上的操作指令大多是无效的。

因此这个普通的NIO模型还是比较鸡肋的,最严重的问题还是CPU忙等导致CPU利用率下降。

5. BIO的性能瓶颈到底在哪?

首先我们通过3和4的回答,能够知道,实际上在I/O的数据没有彻底被读取完之前,线程或者处于阻塞,或者处于忙等的状态,我们从这里可以感受到,实际上这两种效果差不多,因为线程的代码执行根本就没有进展。

但是这是在单机的环境下的解读,让我们看看网络环境下的IO

假设在网络环境下,有一台机器A读写速度很快,有一台机器B读写速度很慢

  • 按照BIO的方式,只有当双方都完成了I/O才能解除阻塞,A读写速度很快,A迅速地将数据写入到了内核空间,然后由内核空间将数据通过TCP连接发送到接收方,这期间,机器A的进程实际上是完成了任务的,但是由于机器B读写速度很慢,导致它一直处于阻塞状态,但是实际上它并不需要阻塞

这就是BIO的痛点,哪怕进程已经完成了自己的任务,也要等待对方的IO完成,并且完成响应,在这个过程中服务端线程被挂起,导致无法处理更多的请求。

  • 如果按照NIO的方式,这时候当一方发出了I/O请求,并且完成了I/O数据的轮询校验后,就可以执行后面的代码了,机器A执行速度快,它将数据写入到内核空间后,就完成了本次I/O请求,不需要等待对方I/O完成。

这就是NIO的改进点,它能够避免不必要的进程阻塞,而是尽可能地让进程完成自己的任务后就被销毁,在这高并发的环境下,能够有效地控制线程数

那么我们场景复原一下,机器A是我们租的服务器高性能服务器,IO速度非常快,机器B是我们自己的电脑,读写速度一般,如果使用BIO,那么就强制让我们租的服务器跟着我们的普通机器阻塞,这显然是不合理的。

合理的方案应该是将IO数据写入完毕后,释放资源,让这些资源去处理下一次请求

但是你说BIO有什么好处么?NIO有什么坏处么?

我觉得这就像TCPUDP,BIO能够在同一次连接的前提下,接收来自客户端的响应,如果有什么风吹草动都能够马上响应,但是代价是双方都Keep-Alive

6. NIO的忙等问题怎么解决?

可以基于IO多路复用降低忙等带来的性能问题

分析痛点:普通的NIO模型由于存在CPU忙等的问题,导致CPU的无效指令和占用过多,那么解决思路就是尽量减少CPU的干预,减少忙等的过程,一个基本的思路就是事件/信号驱动,当I/O完成后,发出事件信号

为了提高程序的吞吐量,我们要做到:

  • 避免CPU忙等,减少内核的工作量
  • 提高主线程的工作效率,避免主线程阻塞

在IO多路复用模型中,主线程不会阻塞,而是采用一种分发IO请求的思路进行解耦

内核中仅有一个线程利用通道将Socket注册到选择器上,然后监听器负责监听所有的连接

监听的内容是文件描述符,当文件描述符中的关键参数变为就绪的时候,此时就通知用户进程解除阻塞状态就绪向下执行

select模式

select模式解决了忙等的问题,但是没有解决根本问题,因为轮询的这个工作只是交给了内核处理而已,当高并发的时候,遍历轮询导致的CPU占用依然会带来问题,它的改进点还是在于改变了主线程的阻塞式接收请求,提供了底层处理基础

epoll模式

如何解决无效遍历?基于事件驱动,只有当I/O完成的时候才让CPU干预,当I/O没有完成的时候,CPU完全不干预,在epoll模式下,用户进程因为I/O事件阻塞,而只有当IO完成后,发出中断请求,此时恢复用户进程执行,从而避免了大量不必要的遍历轮询

优点:

  • 完成了请求接收的请求处理的解耦,能够提高系统的吞吐量

改进点

  • 这个过程中,用户进程依然会因为I/O事件而被阻塞(注意不是主线程被阻塞)

7. IO多路复用模型中的用户线程阻塞还能再优化吗?

信号驱动I/O:它在I/O执行的数据准备阶段,并不会导致用户线程的阻塞

当用户线程需要等待数据的I/O的时候,会向内核发送一个信号,告诉内核需要些什么数据,然后用户线程就会继续做别的事情了,而当内核中的数据准备好了之后,内核会向用户线程发送一个信号,以告知其数据准备完毕,在那之后马上调用recvfrom去查收数据

这个过程中,由于用户在数据准备阶段不需要再进行阻塞了,但是在数据接收阶段依然是存在一个阻塞的,它需要将数据复制到用户空间中,尽管这个过程非常快,但是依然会存在性能的影响

8. AIO

异步IO真正实现了IO全流程的非阻塞。用户进程发出系统调用后立即返回,内核等待数据准备完成,然后将数据拷贝到用户进程缓冲区,然后发送信号告诉用户进程IO操作执行完毕(与SIGIO相比,一个是发送信号告诉用户进程数据准备完毕,一个是IO执行完毕)。

个人认为,最大区别就在于数据的准备和数据的复制,都不需要用户线程进行干预,都是都由内核完成操作,当内核完成操作后,发送信号通知用户线程进行处理。


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录