1.传统 IO 拷贝
如图所示,发生 4 次上下文切换以及 4 次数据拷贝(2 次 CPU 拷贝以及 2 次 DMA 拷贝)
2.零拷贝的几种方式
- mmap+write
- sendfile
- 带有 DMA 收集拷贝功能的 sendfile
2.1mmap+write
虚拟内存
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有 2 个好处:
- 虚拟内存空间可以远远大于物理内存空间
- 多个虚拟内存可以指向同一个物理地址
正是多个虚拟内存可以指向同一个物理地址 ,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少 IO 的数据拷贝次数啦,示意图如下
mmap
- 用户进程通过
mmap方法
向操作系统内核发起 IO 调用,上下文从用户态切换为内核态 。 - CPU 利用 DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- 上下文从内核态切换回用户态 ,mmap 方法返回。
- 用户进程通过
write
方法向操作系统内核发起 IO 调用,上下文从用户态切换为内核态 。 - CPU 将内核缓冲区的数据拷贝到的 socket 缓冲区。
- CPU 利用 DMA 控制器,把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态 ,write 调用返回。
可以发现,mmap+write
实现的零拷贝,I/O 发生了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝 。
mmap
是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次 CPU 拷贝‘’并且用户进程内存是虚拟的 ,只是映射 到内核的读缓冲区,可以节省一半的内存空间。
2.2 sendfile
- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 将读缓冲区中数据拷贝到 socket 缓冲区
- DMA 控制器,异步把数据从 socket 缓冲区拷贝到网卡,
- 上下文(切换 2)从内核态切换回用户态 ,sendfile 调用返回
sendfile
实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝 。那能不能把 CPU 拷贝的次数减少到 0 次呢?有的,即 带有DMA收集拷贝功能的sendfile
!
2.3 sendfile+DMA scatter/gather
- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 把内核缓冲区中的文件描述符信息 (包括内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区
- DMA 控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
- 上下文(切换 2)从内核态切换回用户态 ,sendfile 调用返回。
可以发现,sendfile+DMA scatter/gather
实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包 DMA 拷贝 。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
3. Java 中的零拷贝
3.1 mmap
Java NIO 有一个 MappedByteBuffer
的类,可以用来实现内存映射
try {
//读取文件
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
//mmap映射读取文件
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
//获取写入通道
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输,从mappedBytebuffer中写入到目标通道
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
3.2 sendfile
FileChannel 的 transferTo()/transferFrom()
,底层就是 sendfile() 系统调用函数。Kafka 这个开源项目就用到它
try {
FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输
readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于