[转帖]Java IO篇:什么是零拷贝?

java,io,什么,拷贝 · 浏览次数 : 0

小编点评

**Java NIO** Java NIO是一种用于网络通信的 IIO (Input/Output) 流框架。 NIO提供了一套易于使用的 API,可以帮助程序员实现各种网络通信操作,包括接收和发送数据、创建缓存、进行文件传输等。 **零拷贝** 零拷贝是一种在数据传输过程中使用多个缓存内存来实现内存数据传输的技术。 零拷贝可以减少数据传输的成本,因为程序员可以利用缓存内存来存储数据,并从缓存内存中读取数据,从而减少数据传输的成本。 **零拷贝的应用场景** 零拷贝的应用场景包括: * **文件传输**:零拷贝可以用于高效的文件传输,因为程序员可以利用缓存内存来存储数据,并从缓存内存中读取数据,从而减少数据传输的成本。 * **缓存操作**:零拷贝可以用于创建和缓存数据,因为程序员可以利用缓存内存来存储数据,并从缓存内存中读取数据,从而减少数据传输的成本。 * **网络通信**:零拷贝可以用于实现高效的网络通信,因为程序员可以利用缓存内存来存储数据,并从缓存内存中读取数据,从而减少数据传输的成本。 **零拷贝的实现** 零拷贝的实现可以根据不同的编程语言和框架的不同实现。 在 Java 中,可以使用 `mmap` 和 `write` 方法实现零拷贝。 在 Apache Kafka 中,可以使用 `CompositeByteBuf` 类实现零拷贝。

正文

        在介绍零拷贝的IO模式之前,我们先简单了解下传统的IO模式是怎么样的?

一、传统的IO模式:

传统的IO模式,主要包括 read 和 write 过程:

  • read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区
  • write:先把数据写入到 socket缓冲区,最后写入网卡设备

 流程图如下:

  • (1)用户空间的应用程序通过read()函数,向操作系统发起IO调用,上下文从用户态到切换到内核态,然后再通过 DMA 控制器将数据从磁盘文件中读取到内核缓冲区
  • (2)接着CPU将内核空间缓冲区的数据拷贝到用户空间的数据缓冲区,然后read系统调用返回,而系统调用的返回又会导致上下文从内核态切换到用户态
  • (3)用户空间的应用程序通过write()函数向操作系统发起IO调用,上下文再次从用户态切换到内核态;接着CPU将数据从用户缓冲区复制到内核空间的 socket 缓冲区(也是内核缓冲区,只不过是给socket使用),然后write系统调用返回,再次触发上下文切换
  • (4)最后异步传输socket缓冲区的数据到网卡,也就是说write系统调用的返回并不保证数据被传输到网卡

        在传统的数据 IO 模式中,读取一个磁盘文件,并发送到远程端的服务,就共有四次用户空间与内核空间的上下文切换,四次数据复制,包括两次 CPU 数据复制,两次 DMA 数据复制。但两次 CPU 数据复制才是最消耗资源和时间的,这个过程还需要内核态和用户态之间的来回切换,而CPU资源十分宝贵,要拷贝大量的数据,还要处理大量的任务,如果能把 CPU 的这两次拷贝给去除掉,既能节省CPU资源,还可以避免内核态和用户态之间的切换。而零拷贝技术就是为了解决这个问题

DMA(Direct Memory Access,直接内存访问):DMA 本质上是一块主板上独立的芯片,允许外设设备直接与内存存储器进行数据传输,并且不需要CPU参与的技术

二、什么是零拷贝:

        零拷贝指在进行数据 IO 时,数据在用户态下经历了零次 CPU 拷贝,并非不拷贝数据。通过减少数据传输过程中 内核缓冲区和用户进程缓冲区 间不必要的CPU数据拷贝 与 用户态和内核态的上下文切换次数,降低 CPU 在这两方面的开销,释放 CPU 执行其他任务,更有效的利用系统资源,提高传输效率,同时还减少了内存的占用,也提升应用程序的性能。

        由于零拷贝在内核空间中完成所有的内存拷贝,可以最大化使用 socket 缓冲区的可用空间,从而提高了一次系统调用中处理的数据量,进一步降低了上下文切换次数。零拷贝技术基于 PageCache,而 PageCache 缓存了最近访问过的数据,提升了访问缓存数据的性能,同时,为了解决机械磁盘寻址慢的问题,它还协助 IO 调度算法实现了 IO 合并与预读(这也是顺序读比随机读性能好的原因),这进一步提升了零拷贝的性能。

三、Linux 中的零拷贝方式:

1、mmap + write 实现的零拷贝:

  1. #include <sys/mman.h>
  2. void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
  • addr:指定映射的虚拟内存地址
  • length:映射的长度
  • prot:映射内存的保护模式
  • flags:指定映射的类型
  • fd:进行映射的文件句柄
  • offset:文件偏移量

        在传统 IO 模式的4次内存拷贝中,与物理设备相关的2次拷贝(把磁盘数据拷贝到内存 以及 把数据从内存拷贝到网卡)是必不可少的。但与用户缓冲区相关的2次拷贝都不是必需的,如果内核在读取文件后,直接把内核缓冲区中的内容拷贝到 Socket 缓冲区,待到网卡发送完毕后,再通知进程,这样就可以减少一次 CPU 数据拷贝了。而 内存映射mmap 就是通过前面介绍的方式实现零拷贝的,它的核心就是操作系统把内核缓冲区与应用程序共享,将一段用户空间内存映射到内核空间,当映射成功后,用户对这段内存区域的修改可以直接反映到内核空间;同样地,内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 就不需要在用户态与内核态之间拷贝数据, 提高了数据传输的效率,这就是内存直接映射技术。具体示意图如下:

  • (1)用户应用程序通过 mmap() 向操作系统发起 IO调用,上下文从用户态切换到内核态;然后通过 DMA 将数据从磁盘中复制到内核空间缓冲区
  • (2)mmap 系统调用返回,上下文从内核态切换回用户态(这里不需要将数据从内核空间复制到用户空间,因为用户空间和内核空间共享了这个缓冲区)
  • (3)用户应用程序通过 write() 向操作系统发起 IO调用,上下文再次从用户态切换到内核态。接着 CPU 将数据从内核空间缓冲区复制到内核空间 socket 缓冲区;write 系统调用返回,导致内核空间到用户空间的上下文切换
  • (4)DMA 异步将 socket 缓冲区中的数据拷贝到网卡

        mmap 的零拷贝 I/O 进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝;其中3次数据拷贝中包括了2次 DMA 拷贝和1次 CPU 拷贝。所以 mmap 通过内存地址映射的方式,节省了数据IO过程中的一次CPU数据拷贝以及一半的内存空间

2、sendfile 实现的零拷贝:

  1. #include <sys/sendfile.h>
  2. ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • out_fd:为待写入内容的文件描述符,一个socket描述符。,
  • in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道。
  • offset:指定从读入文件的哪个位置开始读,如果为NULL,表示文件的默认起始位置。
  • count:指定在fdout和fdin之间传输的字节数。

       只要我们的代码执行 read 或者 write 这样的系统调用,一定会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。因此,如果想减少上下文切换次数,就一定要减少系统调用的次数,解决方案就是把 read、write 两次系统调用合并成一次,在内核中完成磁盘与网卡的数据交换。在 Linux 2.1 版本内核开始引入的 sendfile 就是通过这种方式来实现零拷贝的,具体流程图如下:

  • (1)用户应用程序发出 sendfile 系统调用,上下文从用户态切换到内核态;然后通过 DMA 控制器将数据从磁盘中复制到内核缓冲区中
  • (2)然后CPU将数据从内核空间缓冲区复制到 socket 缓冲区
  • (3)sendfile 系统调用返回,上下文从内核态切换到用户态
  • (4)DMA 异步将内核空间 socket 缓冲区中的数据传递到网卡

        通过 sendfile 实现的零拷贝I/O使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。那能不能将CPU拷贝的次数减少到0次呢?答案肯定是有的,那就是 带 DMA 收集拷贝功能的 sendfile

3、带 DMA 收集拷贝功能的 sendfile 实现的零拷贝:

        Linux 2.4 版本之后,对 sendfile 做了升级优化,引入了 SG-DMA技术,其实就是对DMA拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡,无需将内核空间缓冲区的数据再复制一份到 socket 缓冲区,从而省去了一次 CPU拷贝。具体流程如下:

  •  (1)用户应用程序发出 sendfile 系统调用,上下文从用户态切换到内核态;然后通过 DMA 控制器将数据从磁盘中复制到内核缓冲区中
  • (2)接下来不需要CPU将数据复制到 socket 缓冲区,而是将相应的文件描述符信息复制到 socket 缓冲区,该描述符包含了两种的信息:①内核缓冲区的内存地址、②内核缓冲区的偏移量
  • (3)sendfile 系统调用返回,上下文从内核态切换到用户态
  • (4)DMA 根据 socket 缓冲区中描述符提供的地址和偏移量直接将内核缓冲区中的数据复制到网卡

        带有 DMA 收集拷贝功能的 sendfile 实现的 I/O 使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝,这样就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换

备注:需要注意的是,零拷贝有一个缺点,就是不允许进程对文件内容作一些加工再发送,比如数据压缩后再发送。

四、零拷贝技术的应用场景:

1、Java 的 NIO:

(1)mmap + write 的零拷贝方式:

        FileChannel 的 map() 方法产生的 MappedByteBuffer:FileChannel 提供了 map() 方法,该方法可以在一个打开的文件和 MappedByteBuffer 之间建立一个虚拟内存映射,MappedByteBuffer 继承于 ByteBuffer;该缓冲器的内存是一个文件的内存映射区域。map() 方法底层是通过 mmap 实现的,因此将文件内存从磁盘读取到内核缓冲区后,用户空间和内核空间共享该缓冲区。mmap的小demo如下:

  1. public class MmapTest {
  2. public static void main(String[] args) {
  3. try {
  4. FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
  5. MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
  6. FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
  7. //数据传输
  8. writeChannel.write(data);
  9. readChannel.close();
  10. writeChannel.close();
  11. }catch (Exception e){
  12. System.out.println(e.getMessage());
  13. }
  14. }
  15. }

(2)sendfile 的零拷贝方式:

        FileChannel 的 transferTo、transferFrom 如果操作系统底层支持的话,transferTo、transferFrom也会使用 sendfile 零拷贝技术来实现数据的传输

  1. @Override
  2. public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
  3. return fileChannel.transferTo(position, count, socketChannel);
  4. }

sendfile 的 demo 如下:

  1. public class SendFileTest {
  2. public static void main(String[] args) {
  3. try {
  4. FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
  5. long len = readChannel.size();
  6. long position = readChannel.position();
  7. FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
  8. //数据传输
  9. readChannel.transferTo(position, len, writeChannel);
  10. readChannel.close();
  11. writeChannel.close();
  12. } catch (Exception e) {
  13. System.out.println(e.getMessage());
  14. }
  15. }
  16. }

2、Netty 框架:

Netty 的零拷贝主要体现在下面五个方面:

(1)在网络通信上,Netty 的接收和发送 ByteBuffer 采用直接内存,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中(为什么拷贝?因为 JVM 会发生 GC 垃圾回收,数据的内存地址会发生变化,直接将堆内的内存地址传给内核,内存地址一旦变了就内核读不到数据了),然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

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

(3)在缓存操作上,Netty 提供了CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的拷贝。

(4)通过 wrap 操作,我们可以将byte[]数组、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象,进而避免了拷贝操作。

(5)ByteBuf 支持 slice 操作,因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝。

3、kafka:

Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式


参考文章:

(重点推荐)https://juejin.cn/post/6887469050515947528

https://juejin.cn/post/6854573213452599310#heading-8

https://blog.csdn.net/u022812849/article/details/109805403

什么是零拷贝?如何实现零拷贝?

文章知识点与官方知识档案匹配,可进一步学习相关知识
Java技能树IO流概述97422 人正在系统学习中

与[转帖]Java IO篇:什么是零拷贝?相似的内容:

[转帖]Java IO篇:什么是零拷贝?

在介绍零拷贝的IO模式之前,我们先简单了解下传统的IO模式是怎么样的? 一、传统的IO模式: 传统的IO模式,主要包括 read 和 write 过程: read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区write:先把数据写入到 socket缓冲区,最后写入网卡设备 流程图如下: (1)用

[转帖]Java IO篇:什么是 Reactor 网络模型?

一、什么是 Reactor 模型: The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by

[转帖]Java IO篇:什么是 Reactor 网络模型?

一、什么是 Reactor 模型: The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by

[转帖]Java IO篇:序列化与反序列化

1、什么是序列化: 两个服务之间要传输一个数据对象,就需要将对象转换成二进制流,通过网络传输到对方服务,再转换成对象,供服务方法调用。这个编码和解码的过程称之为序列化和反序列化。所以序列化就是把 Java 对象变成二进制形式,本质上就是一个byte[]数组。将对象序列化之后,就可以写入磁盘进行保存或

[转帖]Java IO篇:序列化与反序列化

1、什么是序列化: 两个服务之间要传输一个数据对象,就需要将对象转换成二进制流,通过网络传输到对方服务,再转换成对象,供服务方法调用。这个编码和解码的过程称之为序列化和反序列化。所以序列化就是把 Java 对象变成二进制形式,本质上就是一个byte[]数组。将对象序列化之后,就可以写入磁盘进行保存或

[转帖]解决Java中的java.io.IOException: Broken pipe问题

https://www.cnblogs.com/Chary/p/16835248.html Java 中java.io.IOException: Broken pipe 认识broken pipe pipe是管道的意思,管道里面是数据流,通常是从文件或网络套接字读取的数据。 当该管道从另一端突然关闭

[转帖]JAVA 对象序列化

https://cloud.tencent.com/developer/news/276874 文章来源:企鹅号 - 燃照爱宠物 所谓的『JAVA对象序列化』就是指,将一个JAVA对象所描述的所有内容以文件IO的方式写入二进制文件的一个过程。关于序列化,主要涉及两个流,ObjectInputStre

[转帖]JVM-工具-jcmd

http://events.jianshu.io/p/011f0e3a39ff 一、jcmd 用法 1.1 基本知识 jcmd 是在 JDK1.7 以后,新增了一个命令行工具。 jcmd 是一个多功能的工具,相比 jstat 功能更为全面的工具,可用于获取目标 Java 进程的性能统计、JFR、内存

[转帖]我所知道的线程池

https://bigbully.github.io/%E7%BA%BF%E7%A8%8B%E6%B1%A0 线程池其实或多或少都用过,不过这是我第一次阅读它的源码,包括源码附带的非常详尽的注释。发现我之前对于线程池的理解还是很浅薄的。 其实从ThreadPoolExecutor.java顶部200

【转帖】47.直接内存

目录 1.直接内存概述2.`IO`与`NIO`对比3.直接内存的`OOM`与内存大小设置 1.直接内存概述 1.直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 2.直接内存是在Java堆外,直接向系统申请的内存空间 3.Java的NIO库允许使用直接内存,用于数据