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/O
、I/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
有什么坏处么?
我觉得这就像TCP
和UDP
,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执行完毕)。
个人认为,最大区别就在于数据的准备和数据的复制,都不需要用户线程进行干预,都是都由内核完成操作,当内核完成操作后,发送信号通知用户线程进行处理。