• 阴阳大论之零拷贝


    阴阳大论之零拷贝

    目录

    一、零拷贝

    零拷贝(Zero Copy)

    定义

    • 零拷贝:是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。
    • DMA:硬件和软件的信息传输,可以使用DMA(direct memory access)来完成
    • 将数据从内核空间缓存区copy到用户空间缓存区是消耗CPU的

    优点

    • 减少甚至完全避免不必要的CPU拷贝,从而让CPU解脱出来去执行其他的任务
    • 减少内存带宽的占用
    • 通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换

    零拷贝与非零拷贝对比

    Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。
    Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,
    而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。
    

    Linux操作系统中的零拷贝

    零拷贝实际的实现并没有真正的标准,取决于操作系统如何实现这一点。零拷贝完全依赖于操作系统。操作系统支持,就有;不支持,就没有。不依赖Java本身。

    传统I/O

    1. 程序使用read()系统调用,系统由用户态转换为内核态,磁盘中的数据由DMA(Direct memory access)的方式读取到内核读缓冲区(kernel buffer)。DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中。
    2. 系统由内核态转为用户态,当程序要读的数据已经完全存入内核读缓冲区以后,程序会将数据由内核读缓冲区,写入到用户缓冲区,这个过程需要CPU参与数据的读写。
    3. 程序使用write()系统调用,系统由用户态切换到内核态,数据从用户缓冲区写入到网络缓冲区(Socket Buffer),这个过程需要CPU参与数据的读写。
    4. 系统由内核态切换到用户态,网络缓冲区的数据通过DMA的方式传输到网卡的驱动(存储缓冲区)中(protocol engine)
    //传统IO文件拷贝
    public static void copyFile(String srcFile, String destFile) throws Exception {
        byte[] temp = new byte[1024];
        FileInputStream in = new FileInputStream(srcFile);
        FileOutputStream out = new FileOutputStream(destFile);
        int length;
        while ((length = in.read(temp)) != -1) {
            out.write(temp, 0, length);
        }
    
        in.close();
        out.close();
    }
    

    内核空间内部传输I/O(sendfile)

    1. sendfile()系统调用也会引起用户态到内核态的切换,与内存映射方式不同的是,用户空间此时是无法看到或修改数据内容,也就是说这是一次完全意义上的数据传输过程。
    2. 从磁盘读取到内存是DMA的方式,从内核读缓冲区读取到网络发送缓冲区,依旧需要CPU参与拷贝,而从网络发送缓冲区到网卡中的缓冲区依旧是DMA方式。

    内存映射方式I/O(mmap

    这种方式的I/O原理就是将用户缓冲区(user buffer)的内存地址和内核缓冲区(kernel buffer)的内存地址做一个映射。 Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间; 同样地, 内核空间对这段区域的修改也直接反映用户空间。 正因为有这样的映射关系, 我们就不需要在 用户态(User-space) 与 内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率.

    1. mmap()系统调用首先会使用DMA的方式将磁盘数据读取到内核缓冲区,然后通过内存映射的方式,使用户缓冲区和内核读缓冲区的内存地址为同一内存地址,也就是说不需要CPU再讲数据从内核读缓冲区复制到用户缓冲区。
    2. 当使用write()系统调用的时候,cpu将内核缓冲区(等同于用户缓冲区)的数据直接写入到网络发送缓冲区(socket buffer),然后通过DMA的方式将数据传入到网卡驱动程序中准备发送。

    理想状态下的零拷贝I/O(带有DMA收集拷贝功能的sendfile)

    此种实现方式,并没有将数据拷贝到socket缓冲区,而是拷贝了描述符信息。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上。带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。 这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。

    1. 系统调用sendfile()发起后,磁盘数据通过DMA方式读取到内核缓冲区,内核缓冲区中的数据通过DMA聚合网络缓冲区,然后一齐发送到网卡中。

    JavaNIO中的零拷贝

    NIO中内存映射方式I/O

    JavaNIO中的Channel(通道)就相当于操作系统中的内核缓冲区,有可能是读缓冲区,也有可能是网络缓冲区,而Buffer就相当于操作系统中的用户缓冲区。

    File file = new File("test.zip");
    RandomAccessFile raf = new RandomAccessFile(file, "rw");
    FileChannel fileChannel = raf.getChannel();
    MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
    
    

    NIO中的FileChannel.map()方法其实就是采用了操作系统中的内存映射方式,将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。但是如果其后要通过SocketChannel发送,还是需要CPU进行数据的拷贝。

    processData();
    // 数据处理完成以后,打卡一个SocketChannel
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
    // 这时依旧需要CPU将内核缓冲区的内容拷贝到网络缓冲区
    socketChannel.write(buffer);
    

    NIO中的零拷贝

    File file = new File("test.zip");
    RandomAccessFile raf = new RandomAccessFile(file, "rw");
    FileChannel fileChannel = raf.getChannel();
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
    // 直接使用了transferTo()进行通道间的数据传输
    fileChannel.transferTo(0, fileChannel.size(), socketChannel);
    
    //NIO文件拷贝
    public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
        RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r");
        FileChannel srcFileChannel = srcFile.getChannel();
    
        RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw");
        FileChannel destFileChannel = destFile.getChannel();
    
        long position = 0;
        long count = srcFileChannel.size();
    
        srcFileChannel.transferTo(position, count, destFileChannel);
    }
    
    

    1. transferTo()方法直接将当前通道内容传输到另一个通道,没有涉及到Buffer的任何操作,NIO中的Buffer是JVM堆或者堆外内存,但不论如何他们都是操作系统内核空间的内存。也就是说这种方式不会有内核缓冲区到用户缓冲区的读写问题。
    2. transferTo()的实现方式就是通过系统调用sendfile()(当然这是Linux中的系统调用),根据我们上面所写说这个过程是效率远高于从内核缓冲区到用户缓冲区的读写的。

    Netty中的零拷贝

    通过 CompositeByteBuf 实现零拷贝

    Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝

    通过 wrap 操作实现零拷贝

    通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。对新对象的修改也会反映到 ByteBuf 对象中.

    通过 slice 操作实现零拷贝

    ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。设置不同的参数来获取到 buf 的不同区域的切片

    通过 FileRegion 实现零拷贝

    通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.

    HeapByteBuffer与DirectByteBuffer的区别

    x 存在的位置 优缺点 应用场景
    HeapByteBuffer 分配的内存在JVM堆中,类比用户缓冲区 方便管理和释放
    DirectByteBuffer 在系统级,类比内核缓冲区 减少了数据拷贝,速度快 大文件,socket通信
  • 相关阅读:
    Linux学习笔记-Shell和命令基础
    转载 | 辗转相除法
    C语言字符数组应用示例2:用二维数组输出一个菱形图案
    C语言字符数组应用示例1:编写一个程序,将两个字符串连接起来,不用strcat函数。
    C语言字符数组超细讲解
    C语言二维数组的应用举例
    C语言二维数组超细讲解
    Java同步方法:synchronized到底锁住了谁?
    家乐的深度学习笔记「5」
    工程中的算法应用
  • 原文地址:https://www.cnblogs.com/shiyusen/p/10515388.html
Copyright © 2020-2023  润新知