一文彻底弄懂零拷贝原理以及Java实现
什么是零拷贝
在传统的计算机系统中,在文件从磁盘到达应用程序之前,文件会被存储到内核缓冲区中。当应用程序需要访问文件时,它必须从内核缓冲区将文件读入应用程序的缓冲区。这种方式称之为“传统的拷贝方式”。
但是,“传统的拷贝方式”存在以下问题:
- 内存中存在多个拷贝:原始数据的一个拷贝保存在磁盘中,一个拷贝保存在内核缓冲区中,另一个拷贝保存在应用程序缓冲区中。
- 额外的 CPU 开销:每当一个进程读写文件时,内核必须从内核缓冲区向进程缓冲区复制数据,这需要 CPU 资源。
- 多个拷贝增加了上下文切换:多个拷贝需要上下文切换,同时也会增加网络、存储设备、操作系统之间的交互,这都会导致数据不连续,效率低下。
为了解决这些问题,Linux 系统中引入了彻底的零拷贝技术。这样一来,用户空间的应用程序可以直接访问内核缓冲区中的数据,避免了不必要的拷贝将数据发送给网络和存储设备。
两种实现零拷贝的方式
介绍一下实现零拷贝的两种常见方式:文件描述符传递(sendfile)和内存映射文件(mmap)。
文件描述符传递(sendfile)
“文件描述符传递”利用两个系统调用 —— sendfile 和 splice。简单来说,sendfile 属于 posic/ansi 标准,而 splice 属于 unix 标准,它们都能够减少 CPU 的消耗并避免数据拷贝。
sendfile 函数是把一个文件描述符中的数据传到另一个文件描述符中,它调用 sendfile 函数的文件描述符可以是 socket 或者是普通文件。一旦 socket 和 file 都打且准备好了,数据就可以通过 Socket 并使用 TCP 协议传输。
splice 函数可以把两个管道中数据传输。使用 splice() 函数的优点是在数据移动时不需要将数据从内核空间移动到用户空间,再从用户空间移动到新的文件描述符。splice 可以直接从一个文件描述符移动到另一个文件描述符。splice 功能上和“内存映射文件”有一定的类似之处,它们都是可以避免数据在用户空间和内核空间来回拷贝。
示例:
public static void sendFileBySendFile(File file, OutputStream os) throws IOException {
try (FileInputStream fis = new FileInputStream(file); FileChannel channel = fis.getChannel()) {
long length = file.length();
WritableByteChannel wChannel = Channels.newChannel(os);
channel.transferTo(0, length, wChannel);
}
}
内存映射文件(mmap)
mmap() 系统调用是将一个文件或者其他对象映射到内存中,通常是将一个文件映射到被调用进程的地址空间中,从而可以直接访问文件。内存映射文件的实现可以无需再次拷贝数据,减少了 I/O 操作和消耗。
当进程请求一段文件的内存映射时,内核会以页(通常为 4KB )为单位来设置映射。
映射区域可以当作是文件的缓存,内核也会根据内存写入数据情况而实时刷写到磁盘上。如此一来,进程就可以避免来回复制数据。这里的零拷贝是多个客户端或者系统之间的零拷贝,并不是零次拷贝。
示例:
BufferedInputStream in = new BufferedInputStream(new FileInputStream(new File(filePath)));
FileChannel infileChannel = new FileInputStream(new File(filePath)).getChannel();
MappedByteBuffer buffer = infileChannel.map(FileChannel.MapMode.READ_ONLY, 0, infileChannel.size());
总结
通过零拷贝技术,我们可以让磁盘、网络等协作更高效,在数据的读写和处理方面带来好处。在一些高性能在线服务的场景下,零拷贝技术的优势得以更好的体现。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:一文彻底弄懂零拷贝原理以及java实现 - Python技术站