文章目录
一、介绍
零拷贝(Zero-copy)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
零拷贝技术的主要原理是通过减少数据在用户空间与内核空间之间切换模式的次数,以及减少数据的拷贝次数,来提高数据传输效率。例如,当需要读取一个文件并通过网络发送它时,传统方式下每个读/写周期都需要复制两次数据和切换两次上下文,而零拷贝技术可以将这两个操作合并为一个系统调用(如Linux中的sendfile方法),从而减少上下文切换次数和数据拷贝次数。
在实现零拷贝时,通常依赖于直接存储器访问(DMA)和内存管理单元(MMU)等硬件支持。DMA控制器可以接管数据读写请求,减少CPU的负担,使得CPU能够更高效地完成其他任务。而内存管理单元则可以实现内存映射,使得用户空间可以直接访问内核空间的数据,从而避免了数据的拷贝操作。
零拷贝技术的优点包括提高数据传输效率、减少CPU和内存的使用率、降低上下文切换次数等。然而,零拷贝技术也存在一些缺点,如需要特定的硬件支持、增加了系统的复杂性等。
在Java中,可以通过java.nio.channels包中的FileChannel类来实现零拷贝。其中,transferTo()方法可以将数据从文件通道直接传输到另一个通道,而无需将数据先复制到用户空间再进行传输。
总之,零拷贝技术是一种优化数据传输效率的技术,它通过减少数据拷贝和上下文切换次数来提高系统的性能。在实际应用中,需要根据具体的场景和需求来选择合适的零拷贝技术实现方式。
二、零拷贝的实现方式
零拷贝技术的主要目的是减少或消除数据在传输或处理过程中的内存拷贝次数,以提高性能。以下是实现零拷贝的几种主要方式:
1、直接内存访问(DMA)
- DMA是一种硬件技术,允许外设(如网卡)直接访问内存,绕过CPU的参与,从而实现高速数据传输。
- 数据传输过程中,DMA控制器会接管数据传输的任务,从源地址读取数据并直接写入目的地址,无需CPU的干预。
2、sendfile系统调用
- sendfile系统调用可以在内核态中直接将文件内容发送到网络设备的缓冲区,避免了数据在用户态和内核态之间的拷贝。
- 它通过减少系统调用的次数和内存拷贝的次数,提高了数据传输的效率。
3、splice系统调用
- splice系统调用可以将一个文件描述符的数据直接传输到另一个文件描述符,也可以将数据从一个文件描述符传输到网络设备的缓冲区。
- 它同样避免了中间的拷贝过程,提高了数据传输的效率。
4、mmap和write系统调用
- mmap系统调用可以将文件映射到内存中,然后使用write系统调用将内存中的数据直接发送到网络设备的缓冲区。
- 这种方式避免了数据在用户态和内核态之间的拷贝,减少了系统调用的开销。
5、内存区域映射技术
- 这种方式是在系统内核中存储数据报的内存区域映射到检测程序的应用程序空间,或者是在用户空间建立一缓存,并将其映射到内核空间。
- 检测程序直接对这块内存进行访问,从而减少了系统内核向用户空间的内存拷贝,同时减少了系统调用的开销。
需要注意的是,零拷贝并不是不复制数据,而是减少不必要的数据拷贝次数,从而提升代码性能。在实际应用中,应该根据具体的应用场景和性能要求来选择最合适的零拷贝方式。
三、java中零拷贝的实现方式
在Java中,零拷贝(Zero-Copy)技术主要用于减少数据在传输或处理过程中的内存拷贝次数,从而提高性能。以下是Java中实现零拷贝的几种主要方式:
- FileChannel.transferTo() 和 FileChannel.transferFrom(): 这两个方法允许文件通道(FileChannel)之间直接传输数据,而无需先将数据拷贝到用户空间的缓冲区中。这通常用于网络套接字(SocketChannel)和文件通道之间的数据传输。
FileChannel source = ...; // source file
WritableByteChannel target = ...; // destination (e.g., SocketChannel)
long position = 0;
long count = source.size();
while (count > 0) {
long transferred = source.transferTo(position, count, target);
position += transferred;
count -= transferred;
}
- ByteBuffer.allocateDirect(): 使用直接字节缓冲区(Direct ByteBuffer)可以减少从用户空间到内核空间的内存拷贝。直接缓冲区的内容驻留在JVM堆外的内存,这允许应用程序直接访问物理内存,从而提高了I/O操作的性能。
ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);
// ... write data to buffer ...
FileChannel channel = ...; // get a file channel
channel.write(buffer); // data is written directly from native memory
- Linux下的Sendfile系统调用: 虽然这不是Java直接提供的API,但可以通过Java的本地方法接口(JNI)或Java NIO.2的Files.copy()方法(在某些实现中)来使用。Sendfile允许进程将文件数据从一个文件描述符发送到另一个文件描述符,而无需将数据拷贝到用户空间。
// Java NIO.2's Files.copy() may internally use sendfile on Linux
Path source = ...; // source file
Path target = ...; // destination
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
- MMAP(内存映射文件): 内存映射文件(Memory-Mapped Files)是一种将文件或文件的一部分映射到进程地址空间的技术。通过内存映射,可以像访问内存一样访问文件数据,从而避免了不必要的内存拷贝。Java NIO提供了MappedByteBuffer类来支持内存映射文件。
RandomAccessFile file = new RandomAccessFile("example.txt", "rw");
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
// ... read/write data from buffer ...
四、概念剖析
内部工作流程:
- java本身并不具备IO读写能力,因此read方法调用后,要从java程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用DMA(Direct Memory Access)来实现文件读,期间也不会使用cpu。(DMA也可以理解为硬件单元,用来解放CPU完成文件IO)
- 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即byte[] buf),这期间CPU回参与拷贝,无法利用DMA。
- 调用write方法,这是将数据从用户缓冲区(byte[] buf)写入socket缓冲区,cpu会参与拷贝。
- 接下来要向网卡写数据,这项能力Java又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用DMA将socket缓冲区的数据写入网卡,不会使用CPU。
可以看到中间环节较多,java的IO实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的:
- 用户态与内核态的切换发生了3次,这个操作比较重量级。
- 数据拷贝了4次。
NIO优化
- ByteBuffer.allocate(10) HeapByteBuffer使用的还是java内存。
- ByteBuffer.allocateDirect(10) DirectByteBuffer使用的是操作系统内存。
大部分的步骤与优化前相同。唯一一点:java可以使用DirectByteBuf将对外内存映射到jvm内存中来直接访问使用: - 这块内存不受jvm垃圾回收的影响,因此内存地址固定,有助于IO读写。
- java中的DirectByteBuf对象仅维护了此内存的虚引用,有助于IO读写。
- DirectByteBuf对象被垃圾回收,将虚引用加入引用队列。
- 通过专门线层访问引用队列,根据虚引用释放对外内存。
- 减少了一次数据拷贝,用户态和内核态的切换次数没有减少。
进一步优化(底层采用了linux2.1后提供的sendFile方法),Java中对应着两个channel调用transferTo/transferFrom方法拷贝数据。
- java调用transferTo方法后,要从java程序的用户态切换至内核态,使用DMA将数据读入内核缓冲区,不会使用CPU。
- 数据从内核缓冲区传输到socket缓冲区,cpu会参与拷贝。
- 最后使用DMA将socket缓存区的数据写入网卡,不会使用CPU。
只发生了一次用户态和内核态的切换。
数据拷贝了3次。
进一步优化:linux2.4
- java调用transferTo方法后,要从java程序的用户态切换至内核态,使用DMA将数据读入内存缓冲区,不会使用CPU。
- 只会将一些offset和length信息烤入socket缓冲区,几乎无消耗。
- 使用DMA将内核缓冲区的数据写入网卡,不会使用CPU。
整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了2次。所谓的零拷贝,并不是真正无拷贝,而是在不会拷贝重复数据到JVM内存中,零拷贝的优点:
- 更少的用户态和内核态的切换。
- 不利用CPU计算,减少CPU缓冲伪共享。
- 零拷贝适用于小文件传输。