刚开始学习这个概念的时候我也是一知半解,经常被网上的一些blog绕的不知所云,索性读了几篇优秀的blog,然后慢慢积累,尝试自己去制作一些原理图,再多次阅读比较总结,才逐渐理解其中原理
本文从几个方面来理解零拷贝的概念
- 把磁盘中某个文件内容发送到远程服务器上的流程图
- 零拷贝的含义
- 程序中如何实现零拷贝
- 零拷贝用到的技术
- 总结
- demo演示
1. 把磁盘中某个文件内容发送到远程服务器上的流程图
记住几个重要的概念:
1) 用户空间、内核空间、硬件
2) 读的过程:磁盘文件 -> 读缓冲区(read buffer) -> 用户缓冲区
3) 写的过程:用户缓冲区 -> socket缓冲区(socket buffer)->网卡
4) DMA拷贝技术(Direct Memory Access),直接内存访问技术
理解图片远远比文字来的容易,换成文字可以这样理解,当我们把磁盘中的某个文件发送到远程服务器的时候,我们需要4个拷贝过程
1) 从磁盘中去读取目标文件的内容,拷贝到内核空间的读缓冲区
2) CPU控制器把内核缓冲区的数据拷贝到用户空间的缓冲区
3) 应用程序中调用write()方法把用户空间缓冲区的数据拷贝到内核空间的socket buffer中
4) 把在内核空间的socket buffer中的数据复制到网卡缓冲区,网卡缓冲区再把数据传输到目标服务器
-
零拷贝的含义
数据从磁盘到最终发出需要经历4次拷贝,4次拷贝中,2和3两次是浪费的
零:所谓的零拷贝并不是真正的零拷贝,完全没有数据的拷贝,只是相对用户空间来说,不需要再进 行数据的拷贝,零拷贝只是减少了不必要的拷贝的次数而已
拷贝:数据从一个存储空间拷贝到另外一个存储空间
用户空间和内核空间的切换会带来CPU上下文的切换,对于CPU的性能也会造成影响,所谓的零拷贝就是忽略这两次多余的拷贝,应用程序可以直接把磁盘中的数据从内核中直接传输到socket中,而不需要经过应用程序所在的用户空间,零拷贝通过DMA技术把文件内容复制到内核空间的Read buffer,接着把包含数据长度和位置信息的文件描述符加载到socket buffer中,DMA引擎直接可以把数据从内核空间传输到网卡设备,这个过程中,数据只经历了两次拷贝,就把数据发送到了网卡中,并且减少了两次CPU的上下文切换
-
程序中如何实现零拷贝
在Linux中,零拷贝技术依赖于底层的sendfile()方法去实现的,而在java中,FileChannel.transferTo()方法的底层实现就是sendFile(),好多技术中间件利用零拷贝实现 持久化
-
零拷贝用到的技术
1) DMA拷贝技术
2)传递文件描述符代替数据拷贝
3)mmap:将内核空间读缓冲区的数据直接映射到用户空间,这样就不需要拷贝一份数据了,但是还
是有上下文的切换的
用文件描述符代替数据拷贝的必要条件
1) read buffer 和 socket buffer都在内核空间
2)数据在内核空间未发生任何写操作
- 总结
学习零拷贝技术一定要动手多多实践,再去理解,最后才是贯通,图在心中,自然不慌
- demo演示
/**
* @description:
* @author: Ding Yawu
* @create: 2022/4/25 11:28 AM
*/
public class CopyTest {
@Test
public void normalCopy() throws Exception {
long start = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("file/dyw.zip");
FileOutputStream fos = new FileOutputStream("out.zip");
byte[] buffer = new byte[4096];
while (fis.read(buffer) >= 0){
fos.write(buffer);
}
fos.flush();
long end = System.currentTimeMillis();
//18037
System.out.println("cost time:" + (end - start));
}
//FileChannel 底层就是使用sendFile()方法实现零拷贝
@Test
public void zeroCopy() throws Exception {
long start = System.currentTimeMillis();
File srcFile = new File("file/dyw.zip");
File descFile = new File("out.zip");
FileChannel srcFileChannel = new RandomAccessFile(srcFile, "r").getChannel();
FileChannel descFileChannel = new RandomAccessFile(descFile, "rw").getChannel();
srcFileChannel.transferTo(0, srcFile.length(), descFileChannel);
long end = System.currentTimeMillis();
//4392
System.out.println("cost time:" + (end - start));
}
}