4种常见的IO模型(阻塞IO和非阻塞IO、同步IO和异步IO)

IO模型

1. 什么是I/O?

  1. 直观的层面去理解:IO是计算机与外界设备之间的数据交换的过程。
  2. 从计算机架构去理解:IO是计算机核心(CPU和内存)与其他设备间的数据交换过程。
  3. 从编程角度去理解:IO是应用程序与内存之间的数据交换过程。程序的IO分为两个动作:IO调用IO执行。IO调用是由进程发起,IO执行是操作系统的工作。因此,此时所说的IO是应用程序对操作系统IO功能的一次调用。

Unix 系统下,不论是标准输入还是借助套接字接受网络输入,都有两个步骤:

  1. 等待数据准备好 (Waiting for the data to be ready)
  2. 从内核向进程复制数据 (Copying the data from the kernel to the process)

1.1 IO读写原理

​ 用户程序进行IO的读写,基本上会用到read&write两大系统调用,可能不同操作系统,名称不完全一样,但是功能是一样的。这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统 kernel 内核完成的

  • read系统调用,是把数据从内核缓冲区复制到进程缓冲区;
  • write系统调用,是把数据从进程缓冲区复制到内核缓冲区;
  • 系统kernel内核:负责磁盘和内核缓冲区的数据交换;

2. 计算机内存

2.1 虚拟内存

​ 现代操作系统提供了对主存的抽象概念:虚拟内存(Virtual Memory)。虚拟内存为每个进程提供一个一致私有的地址空间,每个进程拥有一片连续完整的内存空间,让进程有种在独享主存的美好错觉。实际上,虚拟内存通常被分隔成多个物理内存碎片,还有部分暂存在外部磁盘存储器,需要时进行数据交换,加载到物理内存中来。

2.2 内核空间与用户空间

​ 虚拟内存分为 内核空间用户空间 两部分,因为需要避免用户进程直接操作内核。 在 Linux 系统中,进程经过系统调用而陷入内核代码中执行时,称进程处于内核运行态,即 内核态;反之,运行在用户空间执行用户自己的代码时,处于 用户态

2.3 上下文切换

​ 应用程序和内核间无法直接通信,必须通过系统调用,而系统调用的成本很高。当用户进程想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起 系统调用 请求操作系统帮忙完成。而系统调用会产生中断陷入到内核,也就是进行了一次上下文切换操作。(这里可以理解为:同一个进程的 CPU 权限等级的修改)

2.4 进程切换1.3

​ 到了内核,为了控制进程执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换

3. 四种常见的IO模型

3.1 同步阻塞 IO(BIO)

BIO 属于同步阻塞 IO 模型,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间,等待数据、拷贝数据都是处于阻塞状态的。

  • 阻塞IO: 指的是需要内核IO操作彻底完成后,才返回到用户空间执行用户的操作。阻塞指的是用户空间程序的执行状态,用户空间程序需等到IO操作彻底完成,传统的IO模型都是同步阻塞IO,在java中,默认创建的socket都是阻塞的。

  • 同步IO: 是一种用户空间内核空间的调用发起方式。同步IO是指用户空间线程是主动发起IO请求的一方,内核空间是被动接受方。异步IO则反过来,是指内核kernel是主动发起IO请求的一方,用户线程是被动接受方。

在这里插入图片描述

BIO模型读系统调用操作流程举例(socket的read读操作系统调用)

  1. 当用户线程发起 reda 系统调用时,用户线程进入阻塞状态;
  2. 内核(kernel)开始:准备数据 --> 准备就绪 --> 拷贝数据(内核缓冲区拷贝到用户缓冲区);
  3. 返回结果后,用户线程解除阻塞状态(block),重新运行起来;
  • 优点: 程序简单,用户基本不会占用CPU资源

  • 缺点: 高并发场景下,需要大量线程来维护网络连接,内存、线程开销非常大,不能支撑高并发应用场景。

3.2 同步非阻塞IO(NIO)

NIO 属于 同步非阻塞 IO 模型,应用程序会一直发起 read 调用(准备数据阶段不会阻塞),等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。非阻塞IO要求socket被设置为NONBLOCK。

  • 非阻塞IO: 指的是用户程序不需要等待内核IO操作完成后,内核立即返回给用户一个状态值(成功/失败),用户空间无需等到内核的IO操作彻底完成,可以立即返回用户空间,执行用户的操作,处于非阻塞的状态。

简单的说:阻塞是指用户空间(调用线程)一直在等待,而且别的事情什么都不做;非阻塞是指用户空间(调用线程)拿到状态就返回,IO操作可以干就干,不可以干,就去干的事情。但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

在这里插入图片描述

NIO读系统调用操作流程举例:

  1. 当内核数据没有准备好的阶段,用户线程发起IO请求,立即返回读取失败,用户线程不断的发起IO系统调用(没准备好就立马返回)
  2. 内核数据准备好以后,用户线程发起系统调用时,线程阻塞,内核开始复制数据(内核缓冲区 – > 用户缓冲区),然后kernel返回结果
  3. 用户解除block(阻塞)的状态
  • 优点:每次发起IO系统调用时,内核等待数据阶段可以立即返回,用户线程不会阻塞,实时性好

  • 缺点:需要重复发起IO系统调用,不断去轮询会占用大量的CPU时间,系统资源利用率低

3.3 IO多路复用

​ java中的 NIO 属于 多路复用IO模型,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

​ IO多路复用模型的基本原理就是select/epoll系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。因此,好处也就显而易见了——通过一次select/epoll系统调用,就查询到到可以读写的一个甚至是成百上千的网络连接。

​ 目前支持IO多路复用的系统调用,有 select,epoll等等。select系统调用,是目前几乎在所有的操作系统上都有支持,具有良好跨平台特性。epoll是在linux 2.6内核中提出的,是select系统调用的linux增强版本。

在这里插入图片描述

​ 在这种模式中,首先不是进行read系统调动,而是进行select/epoll系统调用。当然,这里有一个前提,需要将目标网络连接,提前注册到select/epoll的可查询socket列表中。然后,才可以开启整个的IO多路复用模型的读流程。

IO多路复用模型读系统调用举例:

  1. 进行select/epoll系统调用(用户线程block阻塞),查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket种的数据准备好了select就会返回。
  2. 用户线程获得目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据(内核缓冲区 --> 用户缓冲区),kernel返回结果
  3. 用户线程解除block的状态。
  • 优点: 用select/epoll的优势在于,它同时处理成千上万的连接,与一条线程维护一个连接相比,IO多路复用大大减小了系统开支。

  • 缺点: select/epoll系统调用,属于同步IO,也是阻塞IO,都需要在读写时间就绪后,自己负责进行读写,读写过程依然是阻塞的。

3.4 异步IO模型(AIO)

AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。

  • 异步IO:,指的是用户空间内核空间的调用方式反过来。用户空间线程是变成被动接受的,内核空间是主动调用者。类似于Java中比较典型的模式是回调模式,用户空线程向内核空间注册各种IO事件的回调函数,由内核去主动调用。(异步IO是基于事件和回调机制实现的)

在这里插入图片描述

异步IO模型系统调用举例:

  1. 用户线程调用了read系统调用后线程不阻塞,可以做其他事情
  2. 内核(kernel)就开始了内核的第一个阶段:准备数据 – 准备就绪 – 拷贝数据(内核缓冲区–>用户缓冲区)
  3. kernel会给用户线程发送一个信号(signal),告诉用户线程read操作完成了
  4. 用户线程读取用户缓冲区的数据,完成后续的业务操作

优点: 内核kernel准备数据和复制数据的两个阶段,用户线程都不是阻塞的

缺点: 需要完成事件的注册与传递,这里面需要底层系统提供大量的支持,目前并不完善

猜你喜欢

转载自blog.csdn.net/weixin_44783506/article/details/129086229