引语
在 java 的传统 IO 中,我们从一个输入流 InputStream 中读取数据,写入另一个输出流 OutStream 中,这种数据传输时基于数据拷贝的,这个过程中涉及到了多次拷贝以及用户态和内核态的切换;这些拷贝和上下文切换操作会导致极大的 CPU 开销,为了提升性能,减少该过程的拷贝以及上下文切换的次数,就出现了零拷贝技术。
零拷贝并代表零次拷贝(后文中将会解释),零拷贝技术是基于操作系统的,只有 OS 中有相应的实现,上层程序才能调用;
传统 IO 的数据传输流程
接下来我们看一下传统 IO 是如何完成数据传输;
场景:在磁盘中读取一个文件通过 Socket 传输出去
从图中可以看出存在四次数据的拷贝,按照拷贝的顺序
- 从磁盘拷贝到内核空间缓冲区,此过程为 DMA copy
- 从内核空间缓冲区拷贝到用户空间缓冲区,此过程为 CPU copy
- 从用户缓冲区拷贝到 Socket 缓冲区,此过程为 CPU copy
- 从 Socket 缓冲区拷贝到网卡,此过程为 DMA copy
我们从用户程序执行的顺序上看,流程如下
- 首先用户程序调用 read ,发生上下文切换,由用户态转为内核态
- 内核将文件从磁盘中拷贝到内核缓冲区
- 内核缓冲区拷贝到用户缓冲区,该过程发生上下文切换,由内核态转为用户态
- 用户程序调用 write ,将数据从用户缓冲区拷贝到 Socket 缓冲区,发生上下文切换,由用户态转为内核态
- 内核将数据从 Socket 缓冲区拷贝到网卡,write 调用返回,发生上下文切换,切换为用户态
在这四次拷贝中存在两次 DMA copy 和两次 CPU copy ,其中 DMA copy 是不需要 CPU 参与的,我们简单理解 DMA copy 是不可避免的(这里不对 DMA 进行介绍),而 CPU copy 是我们需要尽可能减少的
sendFile
在上面的例子中,我们其实只是把一个文件从一个存储拷贝到另一个存储中,其实用户程序的缓冲区是完全可以省略的,这个拷贝过程可以直接在内核中完成;
所以在 Linux 2.1 版本中就出现 sendFile 技术
Linux 2.1版本
我们直接看一下 sendFile 的流程
从图中可以看出用户空间缓冲区被完全去掉了,文件直接从内核缓冲区复制到 Socket 缓冲区,这种方式可以减少一次上下文的切换,和一次 CPU copy;
但是我们可以看到内核空间还存在着两份拷贝(内核缓冲区和 Socket 缓冲区),这也导致多了一次 CPU copy
所以在 Linux 2.4 版本中对 sendFile 进行了优化
Linux 2.4 版本
通过传输文件描述符来避免了这次 CPU copy
在这张图中还存在的 Socket 缓冲区,是由于还存在着一些信息需要拷贝,如文件大小等,但是数据量以及很小了
JAVA 中使用 sendFile
Java NIO 中 tranferform 和 tranferto 就是使用到了 sendFile,但是 win 下是不支持 sendFile 的
mmap
上面介绍了 sendFile ,这项技术确实很不错,可以极大提升性能,但是我们可以看出文件没有被拷贝到用户空间中,这代表着我们只能单纯把文件从一个存储位置拷贝到另一个存储位置,但是要是我们需要在用户程序中对数据进行一些修改的话就无能为力了,所以就出现了 mmap 技术,虽然他不及 sendFile 的性能,但是却更适用,可以让我们在程序中对文件进行更多的操作
mmap 使用了内存映射技术,使得用户程序可以共享内核空间的缓冲区,这样我们可以实现如同传统 IO 中对数据进行随意的修改;
但是这也存在一些问题,由于使用的是共享内核的缓冲区,需要使用同步的方式解决安全问题
JAVA 中使用 mmap
MappedByteBuffer buffer = fc.map(MapMode.READ_WRITE, 0, 1000);
MappedByteBuffer 是抽象类
而 DirectByteBuffer 是 MappedByteBuffer 的实现类,就是使用到了 mmap 技术
ByteBuffer 的 allocateDirect() 方法也是获取 DirectByteBuffer 的方式
引用
文中含上下文切换的图片引用至:https://blog.youkuaiyun.com/may_fly/article/details/104192449/