操作系统之零拷贝原理和实现方式
操作系统之零拷贝原理和实现方式
零拷贝技术是现代操作系统中优化数据传输效率的重要手段。通过减少不必要的数据拷贝和上下文切换,零拷贝能够显著提升系统的整体性能。本文将从传统拷贝的流程入手,详细介绍SendFile、mmap、直接IO和DMA等零拷贝技术的实现原理和应用场景。
传统拷贝的流程
在深入理解零拷贝之前,我们先来看看传统数据拷贝的流程。以文件传输为例,传统拷贝通常涉及以下步骤:
- 用户空间到内核空间:应用程序发起系统调用,操作系统将数据从磁盘读取到内核空间的缓冲区(DMA 搬运)。
- 内核空间到用户空间:操作系统把内核缓冲区的数据拷贝到用户空间的缓冲区,此时应用程序才能访问这些数据。
- 用户空间到内核空间(socket 缓冲区):应用程序将用户空间缓冲区的数据再次拷贝到内核空间的 socket 缓冲区。
- 内核空间到网络设备:最后,操作系统将 socket 缓冲区的数据发送到网络设备,通过网络传输。
在这个过程中,如果不考虑用户态的内存拷贝和物理设备到驱动的数据拷贝,我们会发现,这其中会涉及4次数据拷贝。同时也会涉及到4次进程上下文的切换。这种方式存在多次上下文切换和数据拷贝,效率较低。上下文切换会消耗 CPU 资源,多次数据拷贝也会增加内存带宽的占用和时间开销。但是对于零拷贝来说,是通过各种方式减少拷贝的次数或者cpu的拷贝次数实现的。
常见的零拷贝方式有mmap,sendfile,dma,directI/O等。
SendFile
SendFile 是一种通过减少数据拷贝次数来优化文件传输效率的技术。其本质是只做文件传输,而不通过用户态进行干预,从而减少拷贝次数。这是一种纵向优化的方式。
在 Java NIO 中,FileChannel
提供了 transferTo()
和 transferFrom()
方法,这些方法基于操作系统的 sendfile 机制实现。sendfile 允许在内核空间直接将文件数据从磁盘读取到内核缓冲区,然后直接将内核缓冲区的数据发送到网络设备,避免了数据在用户空间的拷贝。
使用 SendFile 后,数据拷贝次数减少到2次,上下文切换减少到2次。
示例代码
以下是使用 transferTo()
方法的示例代码:
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
public class ZeroCopyTransferToExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileChannel fileChannel = fis.getChannel();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {
// 将文件通道的数据传输到套接字通道
long transferred = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("Transferred bytes: " + transferred);
} catch (IOException e) {
e.printStackTrace();
}
}
}
对于 UDP 数据传输也可以实现SendFIle,可以使用 DatagramChannel 结合 FileChannel.transferTo() 实现零拷贝。同样是利用 transferTo() 方法的特性,直接在内核空间完成数据从文件到网络的传输。
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.nio.channels.FileChannel;
public class ZeroCopyUDPExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileChannel fileChannel = fis.getChannel();
DatagramChannel datagramChannel = DatagramChannel.open()) {
// 绑定本地地址
datagramChannel.bind(new InetSocketAddress(9090));
// 设置目标地址
InetSocketAddress targetAddress = new InetSocketAddress("localhost", 9091);
// 将文件通道的数据传输到 UDP 通道
long transferred = fileChannel.transferTo(0, fileChannel.size(), datagramChannel);
System.out.println("Transferred bytes: " + transferred);
} catch (IOException e) {
e.printStackTrace();
}
}
}
mmap
mmap(内存映射)是一种将内核态和用户态的内存映射到一起的技术,避免了来回拷贝。通过 mmap,进程可以直接通过指针读写共享的内存区域,系统会自动将脏页面回写到对应的文件磁盘上,从而完成文件操作而无需调用 read、write 等系统调用函数。
在 Java NIO 中,MappedByteBuffer
类使用 mmap 系统调用将文件映射到用户空间的地址空间,这样内核和用户空间可以共享这块内存,避免了从内核空间到用户空间的一次拷贝。
使用 mmap 后,数据拷贝次数减少到2次,上下文切换减少到2次。
示例代码
以下是使用 MappedByteBuffer
的示例代码:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class ZeroCopyMappedByteBufferExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileChannel inChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("destination.txt");
FileChannel outChannel = fos.getChannel()) {
// 获取文件大小
long fileSize = inChannel.size();
// 将输入文件映射到内存
MappedByteBuffer mappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
// 将映射的内存数据写入输出文件
outChannel.write(mappedByteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
直接IO
直接IO(Direct I/O)是一种让硬件数据不经过内核态空间,直接到用户态内存的技术。这种方式下,用户态的写入就是直接写入到磁盘,不会再经过操作系统刷盘处理。虽然这减少了拷贝次数,但同时也意味着操作系统不再负责缓存管理,这部分工作需要应用程序自己完成。
Java 并没有直接提供 Direct I/O 的 API,需要通过调用 C 底层代码来实现。这种方式的数据拷贝次数减少到1次,上下文切换减少到2次。
DMA外部实现
DMA(直接内存访问)是一种允许外部设备(如磁盘、网卡等)直接与系统内存进行数据传输的技术。DMA 控制器可以直接控制数据在内存和外部设备之间的传输,CPU 只需要发起 DMA 传输请求,之后就可以去处理其他任务,当 DMA 传输完成后会通过中断通知 CPU。
DMA 主要有两种方式:
- 磁盘到网络的拷贝:这种方式本质是调用 sendfile 机制 + DMA 控制器,绕过了 CPU 处理。
- 网络到网络的拷贝:本质也是通过 DMA 直接将数据写入到内核缓冲区中,并由 DMA 进行转发和读取,也会绕过 CPU 处理。
通过这些零拷贝技术,系统可以在数据传输过程中显著减少不必要的数据拷贝和上下文切换,从而提升整体性能。在实际应用中,选择合适的零拷贝技术需要根据具体场景和需求来决定。