现在的位置: 首页 > 算法 > 正文

什么是零拷贝?Java 中的零拷贝实例

2020年02月19日 算法 ⁄ 共 2266字 ⁄ 字号 评论关闭

  在Java 的三大 IO 模型,都会说到其中的 Non-Blocking IO 就不得不提零拷贝技术,你知道零拷贝技术吗?

  什么是零拷贝

  想要弄清楚什么是零拷贝,首先得明确一个问题,这里的拷贝指的是什么?我们这里所描述的 拷贝 指的是在应用程序中将文件从 A 拷贝到 B,其中的 A 和 B 可以是电脑上的磁盘文件,也可以是网络中的文件。像这样的拷贝操作在操作系统中经历了复杂的操作,首先应用程序发起读取文件操作,读取到文件后又发起写入文件操作或者写到网络中去。

  传统的数据传输

  了解传统数据传输之前,我们要明确用户态和内核态 2 个概念:用户态 是非特权执行状态,该状态下运行的程序被操作系统禁止进行一些危险操作,例如写入系统配置文件,杀死其他用户进程,重启系统,不可直接访问硬件设备。内核态 是高级别特权执行状态,运行在该状态下的程序通常为操作系统程序,具有高的特权级别,拥有访问设备的所有权限。读、写操作,在我们看来是一个完整的操作,其实在操作系统内部将读操作又进行了细化拆分。首先操作系统需要从用户态切换到内核态,在内核态将文件内容从磁盘读取到内核空间缓冲区中。然后切换到用户态,将文件内容从内核空间缓冲区读取到用户空间。而写操作正好是一个逆向的过程,程序需要先将文件内容写到内核空间缓冲区,然后从用户态切换到内核态,再将内容写到磁盘里,最后切换回用户态。图示为如下:聪明的宝宝肯定注意到了,这其中涉及到了 4 次的上下文切换和 4 次的数据拷贝操作,其中磁盘与内核态进行的拷贝操作应用了 DMA 技术(全称 Direct Memory Access,它是一项由硬件设备支持的 IO 技术,应用这项技术可以更好的利用 CPU 资源,在此期间 CPU 可以去做其他事情),而内核态缓冲区到应用程序传输数据需要 CPU 的参与,在此期间 CPU 不能做其他工作。

  mmap 提升拷贝性能

  mmap 是一种内存映射的方法,它可以将磁盘上的文件映射进内存。用户程序可以直接访问内存即可达到访问磁盘文件的效果,这种数据传输方法的性能高于将数据在内核空间和用户之间来回拷贝操作,通常用于高性能要求的应用程序中。因为用户态和内核态的上下文切换以及 CPU 数据拷贝是耗时的操作,所以可以考虑减少数据传输过程中的上下文切换和繁多的 CPU 拷贝数据操作,从而来提升数据传输性能。采用 mmap 来代替 read 系统调用可以有效减少内核空间到用户空间之间的 CPU 拷贝数据操作,于是就诞生了如下的工作情形:观察如上拷贝示意图,我们可以发现此时上下文切换操作缩减到了 2 次,应用程序发起拷贝操作后切换到内核态,数据直接在内核态完成传输而不需要拷贝到用户态,但是此处仍然进行了 3 次拷贝操作,其中还包含一次耗时的 CPU 拷贝操作。别着急,接下来我们看看终极版零拷贝。

  零拷贝技术

  我们知道 DMA 技术是高效的,因此只要去除掉 CPU 拷贝操作即可大大的提升性能。在 Linux 内核 2.1 版本中引入了 sendfile 系统调用,采用这种方式后内核态的缓冲区之间不再进行 CPU 拷贝操作,只需要将源数据的地址和长度告诉目标缓冲区,然后直接采用 DMA 技术将数据传输到目的地,如下图示:如上采用 sendfile 已经剔除了所有耗时的 CPU 拷贝操作,相比于传统的数据拷贝操作性能更高,这就是所谓的零拷贝技术。

  Java 中的零拷贝

  使用传统的文件拷贝方式在 Java 你会看到如下样板代码:

  try (FileInputStream fis = new FileInputStream("sourceFile.txt"); FileOutputStream fos = new FileOutputStream("targetFile.txt")) { byte datas[] = new byte[1024*8]; int len = 0; while((len = fis.read(datas)) != -1){ fos.write(datas, 0, len); }}

  使用如上方式进行文件拷贝的内在执行原理就如我们开头的介绍的那样,经过了多次用户态和内核态的切换,并且伴随着耗时的 CPU 拷贝操作,可想而知在遇到大文件拷贝时候效率会比较低下,此时可以考虑使用零拷贝技术。在Java 1.4 中, FileChannel 的 transferTo 方法即引入了零拷贝技术,让我们来一起看一下,如何使用它来提升性能吧。

  RandomAccessFile sourceFile = new RandomAccessFile("sourceFile.txt", "rw");FileChannel fromChannel = sourceFile.getChannel();RandomAccessFile targetFile = new RandomAccessFile("targetFile.txt", "rw");FileChannel toChannel = targetFile.getChannel();fromChannel.transferTo(0, fromChannel.size(), toChannel);

  如上我们首先获取 FilleChannel,然后调用 FileChannel 的 transferTo 方法即可实现零拷贝操作。内在执行原理就是使用 sendfile 系统调用,剔除了耗时的 CPU 拷贝操作,同时用户态和内核态的上下文切换也是最少的,当你遇到文件拷贝的性能问题时,你可以考虑一下 FilleChannel。FileChannel 中还提供了其他的方法,例如 transferFrom 方法,感兴趣的小伙伴可以自己尝试一下。

抱歉!评论已关闭.