• IO模型个人理解


    1、从I / O开始说起

    我个人理解的IO就是计算机和磁盘、网卡等设备进行交互、信息传输的过程。
    从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
    从应用程序方面来看,I/O涉及了用户态和内核态
    像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
    用户空间的程序不能直接访问内核空间。
    因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
    这里就出现了以下的拷贝
    在这里插入图片描述
    当用户发起I/O请求,会发生如下动作:

    1. 用户请求读取数据
    2. read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 cpu
    3. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA
    4. 调用write()方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝
    5. 向网卡写数据,又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

    可以看到,用户态与内核态的切换发生了 3 次,数据交换了四次。

    2、IO模型

    2.1 分类

    在广义上,IO模型分为五类,分别是:阻塞IO,非阻塞IO,多路复用IO,信号驱动IO,异步IO

    2.2 关于阻塞非阻塞、同步和异步

    以Java的NIO为例,当调用一次 channel.read 或 stream.read 后,会切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:

    • 等待数据阶段
    • 复制数据阶段

    在这里插入图片描述
    对于阻塞而言,如果没有read到数据,就一直等到这里,这就是阻塞的
    如果没有read到数据,但是程序可以返回,就是非阻塞的

    阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务。函数只有在得到结果之后才会返回

    而对于同步来说,发出read后,当有数据了,我再去复制数据
    但是异步来说,我发出了read,有其他的线程得到数据,并别将数据返回给发出read的线程,发出read的线程只需要接收就可以了,不用管等待和发出数据,这就是异步
    异步的概念和同步相对。当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者

    2.3 关于这五个IO模型的图式

    阻塞IO

    在这里插入图片描述
    从发出read请求后,直到数据读到用户线程,在这个过程中,用户线程一直处于阻塞状态

    非阻塞IO

    在这里插入图片描述

    • 用户线程在一个循环中一直调用read方法,若内核空间中还没有数据可读,立即返回
      只是在等待阶段非阻塞
    • 用户线程发现内核空间中有数据后,等待内核空间执行复制数据,待复制结束后返回结果

    多路复用

    在这里插入图片描述
    Java用selector监听事件,当有有事件发生时,select从阻塞状态返回,接着处理发生的多个事件

    异步 IO

    在这里插入图片描述
    发出read请求,用户线程继续执行,当复制数据的步骤完成,线程2将数据带回给用户线程。

    阻塞 IO vs 多路复用

    在这里插入图片描述
    在这里插入图片描述

    • 阻塞IO模式下,若线程因accept事件被阻塞,发生read事件后,仍需等待accept事件执行完成后,才能去处理read事件
    • 多路复用模式下,一个事件发生后,若另一个事件处于阻塞状态,不会影响该事件的执行

    3、零拷贝

    零拷贝指的是数据无需拷贝到 JVM 内存中,同时具有以下三个优点

    • 更少的用户态与内核态的切换
    • 不利用 cpu 计算,减少 cpu 缓存伪共享
    • 零拷贝适合小文件传输

    3.1 传统IO问题

    传统的 IO 将一个文件通过 socket 写出

    File f = new File("data.txt");
    RandomAccessFile file = new RandomAccessFile(file, "r");
    
    byte[] buf = new byte[(int)f.length()];
    file.read(buf);
    
    Socket socket = ...;
    socket.getOutputStream().write(buf);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    内部工作流程是这样的:
    在这里插入图片描述
    就如 上面说的一样:
    内核态和用户态直接出现了三次切换,被数据拷贝了四次

    3.2 NIO优化

    通过 DirectByteBuf

    • ByteBuffer.allocate(10) HeapByteBuffer 使用的还是 java 内存
    • ByteBuffer.allocateDirect(10) DirectByteBuffer 使用的是操作系统内存
      在这里插入图片描述
      大部分步骤与优化前相同,唯有一点:Java 可以使用 DirectByteBuffer 将堆外内存映射到 JVM 内存中来直接访问使用
    • 这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写
    • java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步
      • DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列
      • 通过专门线程访问引用队列,根据虚引用释放堆外内存
    • 减少了一次数据拷贝,用户态与内核态的切换次数没有减少

    3.3 进一步优化

    进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据
    在这里插入图片描述

    1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
    2. 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝
    3. 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

    可以看到

    • 只发生了一次用户态与内核态的切换
    • 数据拷贝了 3 次

    3.4 零拷贝

    在这里插入图片描述

    1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
    2. 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
    3. 使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 cpu

    整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有

    • 更少的用户态与内核态的切换
    • 不利用 cpu 计算,减少 cpu 缓存伪共享
    • 零拷贝适合小文件传输
  • 相关阅读:
    【文献阅读】【NMI 2022】LocalTransform :基于广义模板的有机反应性准确预测图神经网络
    采集侠-免费采集侠-免费采集侠插件
    Java基础二十四(集合框架)
    使用css结合js实现html文件中的双行混排
    【源码编译】android-8.0.0_r21 for Pixel 2 XL for Youpk on ubuntu20.04-server
    【Mysql】Mysql中的B+树索引(六)
    蓝桥杯 选择排序
    Linux exec 命令和Python exec 函数 区别
    SparkSQL之LogicalPlan概述
    #智能车项目(三)串口初始化
  • 原文地址:https://blog.csdn.net/weixin_45483328/article/details/126310208