零拷贝的概念介绍
“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.
零拷贝表示:在计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。
操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
用户空间:普通应用程序使用的虚拟内存空间称为用户空间,用户空间中的代码运行在较低的特权级别上,只能看到允许它们使用的部分系统资源,并且不能使用某些特定的系统功能,也不能直接访问内核空间和硬件设备,以及其他一些具体的使用限制。
内核空间:一部分为核心软件,即是kernel,也称作内核空间,Linux 操作系统和驱动程序运行在内核空间。
DMA(Direct Memory Access):是一种不经过CPU而直接从内存存取数据的数据交换模式。在DMA模式下,CPU只须向DMA控制器下达指令,让DMA控制器来处理数据的传送,数据传送完毕再把信息反馈给CPU,这样就很大程度上减轻了CPU资源占有率,可以大大节省系统资源。
零拷贝技术
普通文件一次读写涉及到四次数据文件的拷贝、四次用户模式和内核模式之间的上下文切换。
第一次复制:DMA从磁盘读取文件内容存储在内核空间的缓冲区
第二次复制:数据从内核空间缓冲区复制到用户空间缓冲区
第三次复制:数据从用户空间缓冲区复制到socket特定缓冲区(内核缓冲区)
第四次复制:数据从内核缓冲区传递至协议引擎
在linux 2.4及以上版本的内核中(如linux 6或centos 6以上的版本),开发者修改了socket buffer descriptor,使网卡支持 gather operation,通过kernel进一步减少数据的拷贝操作。这个方法不仅减少了上下文切换,还消除了和CPU有关的数据拷贝。
使用零拷贝技术后,文件读写只需要两次拷贝。把普通文件的读写的第二次和第三次拷贝省去。
第一次复制:DMA从磁盘读取文件内容存储在内核空间的缓冲区
第二次复制:数据从内核缓冲区传递至协议引擎
Zero Copy的模式中,避免了数据在用户空间和内核空间之间的拷贝,避免消耗CPU周期和内存带宽,从而提高了系统的整体性能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。
零拷贝的使用场景
考虑从文件读取并通过网络将数据传输到另一个程序的情况。(静态内容的Web应用程序,FTP服务器,邮件服务器等等)
Kafka、Netty等都有实际的应用场景。
【引用Netty中的零拷贝】【Netty的零拷贝体现在三个方面: 1. Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。 2. Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。 3. Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。】
零拷贝的Java代码实现
拷贝1G的文件,在Mac上运行无法体现差距。。
使用零拷贝的实现,Mac上运行时长:22s152ms
1 2 3 4 5 6 7 8 9 10 11 12 | public void copyFile(String from, String to) throws IOException { FileChannel fromChannel = new RandomAccessFile(from, "rw").getChannel(); FileChannel toChannel = new RandomAccessFile(to, "rw").getChannel(); long position = 0; long count = fromChannel.size(); fromChannel.transferTo(position, count, toChannel); fromChannel.close(); toChannel.close(); } |
正常文件拷贝的实现,Mac上运行时长:23s548ms
1 2 3 4 5 6 7 8 9 10 11 12 13 | public void copyFile(String from, String to) throws IOException { FileInputStream input = new FileInputStream(from); FileOutputStream output = new FileOutputStream(to); byte[] b = new byte[1024]; int n = 0; while ((n = input.read(b)) != -1) { output.write(b, 0, n); } input.close(); output.close(); } |
参考资料