在传统的数据 IO 模式中,读取一个磁盘文件,并发送到远程端的服务,就共有四次用户空间与内核空间的上下文切换,四次数据复制,分别是两次 CPU 数据复制,两次 DMA 数据复制。但两次 CPU 数据复制才是最消耗资源和时间的,这个过程还需要内核态和用户态之间的来回切换,而CPU资源十分宝贵,要拷贝大量的数据,还要处理大量的任务,如果能把 CPU 的这两次拷贝给去除掉,既能节省CPU资源,还可以避免内核态和用户态之间的切换。而零拷贝技术就是为了解决这个问题
DMA(Direct Memory Access,直接存储器存取)方式: 外部设备不通过CPU而直接与系统内存进行数据交换的技术
零拷贝指在进行数据 IO 或传输时,数据在用户态下经历了零次拷贝,并非不拷贝数据。通过减少数据传输过程中 内核缓冲区和用户进程缓冲区间不必要的 CPU数据拷贝 与 用户态和内核态的上下文切换次数,降低 CPU 在这两方面的开销,释放 CPU 执行其他任务,更有效的利用系统资源,提高传输效率,同时还减少了内存的占用,也提升应用程序的性能。
由于零拷贝在内核空间中完成所有的内存拷贝,可以最大化使用 socket 缓冲区的可用空间,从而提高了一次系统调用中处理的数据量,进一步降低了上下文切换次数。零拷贝技术基于 PageCache,而 PageCache 缓存了最近访问过的数据,提升了访问缓存数据的性能,同时,为了解决机械磁盘寻址慢的问题,它还协助 IO 调度算法实现了 IO 合并与预读(这也是顺序读比随机读性能好的原因),这进一步提升了零拷贝的性能。
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
在传统 IO 模式的4次内存拷贝中,与物理设备相关的2次拷贝(把磁盘内容拷贝到内存 以及 把内存拷贝到网卡)是必不可少的。但与用户缓冲区相关的2次拷贝都不是必需的,如果内核在读取文件后,直接把内核缓冲区中的内容拷贝到 Socket 缓冲区,待到网卡发送完毕后,再通知进程,这样就可以减少一次 CPU 数据拷贝了。而内存映射 mmap就是通过前面介绍的方式实现零拷贝的,它的核心就是操作系统会把内核缓冲区与应用程序共享,可以将一段用户空间内存映射到内核空间,当映射成功后,用户对这段内存区域的修改可以直接反映到内核空间;同样地,内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 就不需要在用户态(User-space)与内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率,这就是内存直接映射技术。具体示意图如下:
** mmap 的零拷贝 I/O 进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝;其中3次数据拷贝中包括了2次 DMA 拷贝和1次 CPU 拷贝**
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
只要我们的代码执行 read 或者 write 这样的系统调用,一定会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。因此,如果想减少上下文切换次数,就一定要减少系统调用的次数,解决方案就是把 read、write 两次系统调用合并成一次,在内核中完成磁盘与网卡的数据交换。在 Linux 2.1 版本内核开始引入的 sendfile 就是通过这种方式来实现零拷贝的,具体流程图如下:
** 通过 sendfile 实现的零拷贝I/O使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝**
Linux 2.4 版本开始支持,操作系统提供 scatter 和 gather 的 SG-DMA 方式,直接从内核空间缓冲区中将数据读取到网卡,无需将内核空间缓冲区的数据再复制一份到 socket 缓冲区。
** 带有 DMA 收集拷贝功能的 sendfile 实现的 I/O 使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝,这样就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换**
备注:需要注意的是,零拷贝有一个缺点,就是不允许进程对文件内容作一些加工再发送,比如数据压缩后再发送。
(1)mmap + write 的零拷贝方式:
FileChannel 的 map() 方法产生的 MappedByteBuffer:FileChannel 提供了 map() 方法,该方法可以在一个打开的文件和 MappedByteBuffer 之间建立一个虚拟内存映射,MappedByteBuffer 继承于 ByteBuffer;该缓冲器的内存是一个文件的内存映射区域。map() 方法底层是通过 mmap 实现的,因此将文件内存从磁盘读取到内核缓冲区后,用户空间和内核空间共享该缓冲区。
(2)sendfile 的零拷贝方式:
FileChannel 的 transferTo、transferFrom 如果操作系统底层支持的话,transferTo、transferFrom也会使用 sendfile 零拷贝技术来实现数据的传输
Netty 的零拷贝主要体现在下面五个方面:
(1)在网络通信上,Netty 的接收和发送 ByteBuffer 采用直接内存,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中(为什么拷贝?因为 JVM 会发生 GC 垃圾回收,数据的内存地址会发生变化,直接将堆内的内存地址传给内核,内存地址一旦变了就内核读不到数据了),然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
(2)在文件传输上,Netty 的通过 FileRegion 包装的 FileChannel.tranferTo 实现文件传输,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
(3)在缓存操作上,Netty 提供了CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的拷贝。
(4)通过 wrap 操作,我们可以将byte[]数组、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象,进而避免了拷贝操作。
(5)ByteBuf 支持 slice 操作,因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝。
Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式
参考文章:
(重点推荐)https://juejin.cn/post/6887469050515947528
https://juejin.cn/post/6854573213452599310#heading-8
https://blog.csdn.net/u022812849/article/details/109805403
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/a745233700/article/details/122660332
内容来源于网络,如有侵权,请联系作者删除!