从理论到实践:零拷贝技术的全面解读

目录

一、I/O 工作方式

        1.1 DMA

二、零拷贝

        2.1 传统数据拷贝

        2.2 Java 应用数据处理

        2.3. 零拷贝流程

三、零拷贝案例

四、总结


一、I/O 工作方式

        磁盘可以说是计算机系统中最慢的硬件之一,读写速度相差内存10倍以上,所以针对磁盘的优化技术非常多。下面以文件传输为切入线分析 I/O 的工作方式。

        在没有 DMA 技术前,I/O 是这样工作的,如下图:

从理论到实践:零拷贝技术的全面解读_第1张图片

  1. 用户发起 read 调用,CPU 将对应的指令给磁盘控制器,然后返回;
  2. 磁盘控制器收到指令后,开始准备数据,会把数据放到磁盘控制器的内存缓冲区中,然后产生一个中断;
  3. CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的数据一次一个字节的读到自己的寄存器中,然后再把寄存器里的数据写到内存中,而在数据传输期间,CPU 无法执行其它任务。

        可以看到,整个传输过程,都需要 CPU 亲自搬运数据,,而且这个过程是阻塞的,CPU 不能做其他的工作,简单的搬运几个字符问题不大,如果是搬运大量的数据,肯定忙不过来。于是 DMA 技术诞生了。

        1.1 DMA

        DMA(Direct Memory Access,直接内存访问)技术是一种在计算机系统中用于高效数据传输的技术,它允许数据在设备和内存之间直接传输,而无需 CPU 的干预。DMA 技术显著提高了数据传输的速度和系统的整体性能,因为它减少了 CPU 的负担,使得 CPU 可以专注于其他更重要的任务。

        工作流程图如下:

从理论到实践:零拷贝技术的全面解读_第2张图片

  1. 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存中,进程进入阻塞状态;
  2. 操作系统收到请求后,进一步将 I/O 请求发送到 DMA,然后让 CPU 执行其他任务;
  3. DMA 将请求发送到磁盘;
  4. 磁盘收到请求后,把数据从磁盘读取到缓冲区中,当磁盘控制器的缓冲区读满后,向 DMA 发起中断信号,告知自己缓冲区已满;DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区,此时不占用 CPU;
  5. 当 DMA 读了足够多的数据,就会发出中断信号给 CPU;
  6. CPU 收到 DMA 信号,知道数据准备好了,于是将数据从内核拷贝到内核空间,系统调用返回。

        可以看到,整个数据传输过程,CPU不在参与数据搬运的工作,而是全程由DMA完成,但是CPU在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要CPU来告诉DMA控制器。

二、零拷贝

        2.1 传统数据拷贝

        从上述过程可以看到,传统的数据传输模型中,数据在不同系统组件之间传输时,往往需要再用户空间和内核空间之间进行多次拷贝。这种多次数据拷贝不仅消耗 CPU 计算资源,增加了上下文切换的开销,还可能导致较高的延迟和系统吞吐量的下降。在数据较小的情况下,这种影响可能不明显,但在大数据和高速网络环境中,频繁的数据拷贝成为了明显的性能瓶颈。

        下面来看一个例子,了解下传统数据传输的过程。如果服务端要提供文件传输功能,我们能想到的最简单的方式:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

        传统I/O的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,内核空间的数据是通过操作系统层面的I/O接口从磁盘读取或写入的。

从理论到实践:零拷贝技术的全面解读_第3张图片

        在这个过程中共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,分别为 read 和 write,每次系统调用都需先从用户态切换到内核态,等内核任务完成后,在从内核态切换到用户态。

        上下文切换的成本是比较高的,在高并发场景下,上下文切换过多的话,就会被放大,影响整体的性能。

        在这个过程中发生了 4 次拷贝动作,其中两次是 DMA 完成的拷贝,两次是 CPU 来完成的拷贝。

  1. 把磁盘上的数据拷贝到操作系统内核缓冲区里,这个操作是由 DMA 来完成的;
  2. 把内核缓冲区里的数据拷贝到用户的缓冲区中,这时应用程序就能使用这部分数据了,这个拷贝过程是由 CPU 完成的;
  3. 把应用程序中经过处理的数据再拷贝到内核中 socket 缓冲区中,这个过程由 CPU 完成;
  4. 把内核 Socket 缓冲区里的数据拷贝到网卡中,然后就能由网卡发出去了,这个过程由 DMA 完成。

        我们只是搬运一份数据,就涉及到了 4 次数据拷贝,4 次上下文切换,在高并发系统中,这种操作是很糟糕的,严重影响性能。

        2.2 Java 应用数据处理

        这里在扩展一下,在 Java 应用中,实际上可能会涉及 6 次数据拷贝。

        Java 的类实例一般在 JVM 的堆上分配的,而 Java 是通过 JNI 调用 C 代码来完成 socket 通信的,JVM 内存之外的部分叫做本地内存,C 程序代码在运行过程中用到的内存就是本地内存中分配的。

        Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer,下面代码是如何创建两种Buffer。

    那 HeapByteBuffer 和 DirectByteBuffer 有什么区别呢?HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 来接收网络数据,需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆上,而不是直接从内核拷贝到JVM堆上。这是为什么呢?因为数据从内核拷贝到JVM堆上的过程中,JVM 可能会发生 GC,GC 过程中对象可能被移动,也就是说 JVM 堆上的字节数组有可能被移动,这样的话 Buffer 地址就失效了。如果这中间经过本地内存中转,从本地内存到 JVM 堆拷贝的过程中可以保证不做 GC。

        如果使用 HeapByteBuffer,你会发现 JVM 堆和内核之间多了一层中转,而DirectByteBuffer 用来解决这个问题,DirectByteBuffer 对象本身在 JVM 堆上,但是它持有的字节数组不是从 JVM 堆上分配的,而是从本地内存分配的。DirectByteBuffer 对象中有个 Long 类型字段 address,记录着本地内存地址,这样在接收数据的时候直接把这个本地内存地址传给 C程序,C 程序会将网络数据从内核拷贝到这个本地内存,这种方式比 HeapByteBuffer 少了一次拷贝,因此一般来说它的速度比 HeapByteBuffer 快好几倍。

        所以如果在使用中使用的是 HeapByteBuffer,那么数据拷贝就会是 6 次了。

        2.3. 零拷贝流程

        Linux 内核从2.4版本开始,对于网卡支持 SG-DMA 技术的情况下,sendFile() 系统调用的过程发生了变化。

从理论到实践:零拷贝技术的全面解读_第4张图片

  1. 通过 DMA 将磁盘上的数据拷贝到内核缓冲区中;
  2. 缓冲区描述符和数据长度传输到 scoket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓冲区中的数据拷贝到网卡缓冲区里,此过程不需要将数据从操作系统内核拷贝到Socket 内核缓冲区。

        这就是所谓的零拷贝技术,因为我们没有在内存层面去拷贝数据,也就是全程没有通过CPU来搬运数据,所有的数据都通过DMA来进行传输。与传统方式相比,减少了两次上下文切换和数据拷贝次数,只需要两次上下文切换就可以完成文件传输,而且两次数据拷贝都不要CPU的参与,所以,零拷贝技术可以把文件传输的性能提高至少一倍以上。

三、零拷贝案例

        下面看一个简单的案例来对比下零拷贝技术对性能的提升。

public static void traditionalCopy(File input,File output) throws Exception{
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    @Cleanup
    RandomAccessFile read = new RandomAccessFile(input, "rw");
    @Cleanup
    RandomAccessFile write = new RandomAccessFile(output, "rw");
    byte[] buf = new byte[1024];
    int len;
    while ((len = read.read(buf)) != -1) {
        write.write(buf, 0, len);
    }
    stopWatch.stop();
    System.out.println("traditionalCopy耗时" + stopWatch.getTotalTimeSeconds());
}

public static void zeroCopy(File input,File output) throws Exception {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    FileInputStream fis = new FileInputStream(input);
    FileChannel srcChannel = fis.getChannel();
    FileOutputStream fos = new FileOutputStream(output);
    FileChannel destChannel = fos.getChannel();
    srcChannel.transferTo(0, input.length(), destChannel);
    stopWatch.stop();
    System.out.println("zeroCopy耗时:" + stopWatch.getTotalTimeSeconds());
}

        实际测试,一个 816MB 的文件,传统方式的时间大概在 6-12s 之间,用 NIO 零拷贝的方式在 1s 左右,相差悬殊,文件越大性能差距越明显。 

        有很多开源框架都使用了零拷贝技术来提升性能,下面介绍一些:

  • Kafka:Kafka 性能好的原因之一就是使用了零拷贝技术,用零拷贝技术来优化数据的接受、存储和传输过程,从而实现高处理速度。
  • Netty:Netty 是一个异步事件驱动的网络应用程序框架,它使用零拷贝技术和其他高性能网络编程技巧,如非阻塞 I/O 和事件循环,来提供高性能和可扩展的网络服务。
  • Redis:Redis 是一个开源的键值存储系统,它使用 mmap 机制来实现零拷贝,允许直接在内存映射文件上操作,从而提高了数据读写的效率。

四、总结

        零拷贝技术的出现和发展,是为了适应高速网络和大规模数据处理的需要,解决传统数据传输机制的效率低下问题,优化系统资源利用,提高数据传输和处理的速度与性能。随着技术的进步,零拷贝技术在现代操作系统和高性能网络编程框架中得到了广泛应用,成为提高系统效率和响应速度的关键技术之一。

往期经典推荐:

深入理解I/O模型-CSDN博客

Sentinel与Nacos强强联合,构建微服务稳定性基石的重要实践_nacos sentinel-CSDN博客

Kafka VS RabbitMQ,架构师教你如何选择_rabbitmq和kafka选型-CSDN博客

TiDB存储引擎TiKV揭秘-CSDN博客

深入浅出 Drools 规则引擎-CSDN博客

你可能感兴趣的:(Java基础,Java,零拷贝)