前言
大白话解释,零拷贝就是没有把数据从一个存储区域拷贝到另一个存储区域。然而没有数据的复制,怎么可能实现数据的传输呢?其实咱们在 java NIO、netty、kafka 遇到的零拷贝,并不是不复制数据,而是缩小不必要的数据拷贝次数,从而晋升代码性能
- 零拷贝的益处
- 内核空间和用户空间
- 缓冲区和虚拟内存
- 传统的 I/O
- mmap+write 实现的零拷贝
- sendfile 实现的零拷贝
- 带有 DMA 收集拷贝性能的 sendfile 实现的零拷贝
- java 提供的零拷贝形式
关注公众号,一起交换 : 潜行前行
零拷贝的益处
- 缩小或防止不必要的 CPU 数据拷贝,从而开释 CPU 去执行其余工作
- 零拷贝机制能缩小用户空间和操作系统内核空间的上下文切换
- 缩小内存的占用
内核空间和用户空间
- 内核空间:Linux 本身应用的空间;次要提供过程调度、内存调配、连贯硬件资源等性能
- 用户空间:提供给各个程序过程的空间;用户空间不具备拜访内核空间资源的权限,如果应用程序须要应用到内核空间的资源,则须要通过零碎调用来实现:从用户空间切换到内核空间,实现相干操作后再从内核空间切换回用户空间
缓冲区和虚拟内存
-
间接内存拜访(Direct Memory Access)(DMA)
- 间接内存拜访:DMA 容许外设设施和内存存储器之间间接进行 IO 数据传输,其过程不须要 CPU 的参加
-
缓冲区 是所有 I / O 的根底,I/O 无非就是把数据移进或移出缓冲区
- 过程发动 read 申请,内核先查看内核空间缓冲区是否存在过程所需数据,如果曾经存在,则间接 copy 数据到过程的内存区。如果没有,零碎则向磁盘申请数据,通过 DMA 写入内核的 read 缓冲冲区,接着再将内核缓冲区数据 copy 到过程的内存区
- 过程发动 write 申请,则是把过程的内存区数据 copy 到内核的 write 缓冲区,而后再通过 DMA 把内核缓冲区数据刷回磁盘或者网卡中
-
虚拟内存:古代操作系统都应用虚拟内存,有如下两个益处
- 一个以上的虚拟地址能够指向同一个物理内存地址
- 虚拟内存空间可大于理论可用的物理地址
- 利用第一点个性能够把内核空间地址和用户空间的虚构地址映射到同一个物理地址,这样 DMA 就能够填充 (读写) 对内核和用户空间过程同时可见的缓冲区了;大抵如下
传统的 I/O
`#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
ssize_t read(int filedes, void *buf, size_t nbytes);
`
- 如 java 在 linux 零碎上,读取一个磁盘文件,并发送到近程端的服务
- 1)收回 read 零碎调用,会导致用户空间到内核空间的上下文切换,而后再通过 DMA 将文件中的数据从磁盘上读取到内核空间缓冲区
- 2)接着将内核空间缓冲区的数据拷贝到用户空间过程内存,而后 read 零碎调用返回。而零碎调用的返回又会导致一次内核空间到用户空间的上下文切换
- 3)write 零碎调用,则再次导致用户空间到内核空间的上下文切换,将用户空间的过程里的内存数据复制到内核空间的 socket 缓冲区(也是内核缓冲区,不过是给 socket 应用的),而后 write 零碎调用返回,再次触发上下文切换
- 4)至于 socket 缓冲区到网卡的数据传输则是独立异步的过程,也就是说 write 零碎调用的返回并不保证数据被传输到网卡
一共有四次用户空间与内核空间的上下文切换。四次数据 copy,别离是两次 CPU 数据复制,两次 DMA 数据复制
mmap+write 实现的零拷贝
`#include <sys/mman.h>
void mmap(void start, size_t length, int prot, int flags, int fd, off_t offset)
`
- 1)收回 mmap 零碎调用,导致用户空间到内核空间的上下文切换。而后通过 DMA 引擎将磁盘文件中的数据复制到内核空间缓冲区
- 2)mmap 零碎调用返回,导致内核空间到用户空间的上下文切换
- 3)这里不须要将数据从内核空间复制到用户空间,因为用户空间和内核空间共享了这个缓冲区
- 4)收回 write 零碎调用,导致用户空间到内核空间的上下文切换。将数据从内核空间缓冲区复制到内核空间 socket 缓冲区;write 零碎调用返回,导致内核空间到用户空间的上下文切换
- 5)异步,DMA 引擎将 socket 缓冲区中的数据 copy 到网卡
通过 mmap 实现的零拷贝 I / O 进行了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝;其中 3 次数据拷贝中包含了 2 次 DMA 拷贝和 1 次 CPU 拷贝
sendfile 实现的零拷贝
`#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
`
- 1)收回 sendfile 零碎调用,导致用户空间到内核空间的上下文切换,而后通过 DMA 引擎将磁盘文件中的内容复制到内核空间缓冲区中,接着再将数据从内核空间缓冲区复制到 socket 相干的缓冲区
- 2)sendfile 零碎调用返回,导致内核空间到用户空间的上下文切换。DMA 异步将内核空间 socket 缓冲区中的数据传递到网卡
通过 sendfile 实现的零拷贝 I / O 应用了 2 次用户空间与内核空间的上下文切换,以及 3 次数据的拷贝。其中 3 次数据拷贝中包含了 2 次 DMA 拷贝和 1 次 CPU 拷贝
带有 DMA 收集拷贝性能的 sendfile 实现的零拷贝
- 从 Linux 2.4 版本开始,操作系统提供 scatter 和 gather 的 SG-DMA 形式,间接从内核空间缓冲区中将数据读取到网卡,无需将内核空间缓冲区的数据再复制一份到 socket 缓冲区
- 1)收回 sendfile 零碎调用,导致用户空间到内核空间的上下文切换。通过 DMA 引擎将磁盘文件中的内容复制到内核空间缓冲区
- 2)这里没把数据复制到 socket 缓冲区;取而代之的是,相应的描述符信息被复制到 socket 缓冲区。该描述符蕴含了两种的信息:A)内核缓冲区的内存地址、B)内核缓冲区的偏移量
- 3)sendfile 零碎调用返回,导致内核空间到用户空间的上下文切换。DMA 依据 socket 缓冲区的描述符提供的地址和偏移量间接将内核缓冲区中的数据复制到网卡
带有 DMA 收集拷贝性能的 sendfile 实现的 I / O 应用了 2 次用户空间与内核空间的上下文切换,以及 2 次数据的拷贝,而且这 2 次的数据拷贝都是非 CPU 拷贝。这样一来咱们就实现了最现实的零拷贝 I / O 传输了,不须要任何一次的 CPU 拷贝,以及起码的上下文切换
java 提供的零拷贝形式
- java NIO 的零拷贝实现是基于 mmap+write 形式
- FileChannel 的 map 办法产生的 MappedByteBuffer FileChannel 提供了 map()办法,该办法能够在一个关上的文件和 MappedByteBuffer 之间建设一个虚拟内存映射,MappedByteBuffer 继承于 ByteBuffer;该缓冲器的内存是一个文件的内存映射区域。map 办法底层是通过 mmap 实现的,因而将文件内存从磁盘读取到内核缓冲区后,用户空间和内核空间共享该缓冲区。用法如下
`public void main(String[] args){
try {
FileChannel readChannel = FileChannel.open(Paths.get(“./cscw.txt”), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Paths.get(“./siting.txt”), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 1024 40);
// 数据传输
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
`
- FileChannel 的 transferTo、transferFrom 如果操作系统底层反对的话,transferTo、transferFrom 也会应用相干的零拷贝技术来实现数据的传输。用法如下
`public void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get(“./cscw.txt”), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Paths.get(“./siting.txt”), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
long len = readChannel.size();
long position = readChannel.position();
// 数据传输
readChannel.transferTo(position, len, writeChannel);
// 成果和 transferTo 一样的
//writeChannel.transferFrom(readChannel, position, len,);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
`
欢送指注释中谬误
参考文章
- 浅谈 Linux 下的零拷贝机制[1]
- 面试被问到“零拷贝”!你真的了解吗?[2]
- java NIO 的通道 Channel 的了解[3]
- Channel 根本应用——FileChannel 类和内存映射的应用[4]
参考资料
[1]
浅谈 Linux 下的零拷贝机制: https://www.jianshu.com/p/e76…
[2]
面试被问到“零拷贝”!你真的了解吗?: https://my.oschina.net/u/3990…
[3]
java NIO 的通道 Channel 的了解: https://blog.csdn.net/qq_2709…
[4]
Channel 根本应用——FileChannel 类和内存映射的应用: https://blog.csdn.net/qq_4533…