有关Zero-Copy、MMap以及DirectByteBuffer的一切

有一个很常见的场景,比如需要将文件从磁盘上原封不动地发送到网络的另一端。这通过代码实现起来很简单:对于Java而言,你可以使用InputStream的某个实现类将文件一块块地读取到小的缓冲区(通常我们都将缓冲区大小设置为8KB),然后再将缓冲区数据输出到OutputStream中。更好的做法是你可以创建一个PipedInputStream实例,让它来管理缓冲区。但是,如果你的应用对性能有要求,那么通过这种方式去读取文件再发送在操作系统层面来看就显得有些太耗资源了。
为什么这样说呢?结合下图我来解释下原因
在这里插入图片描述

  1. JVM执行read()系统调用;
  2. 操作系统从用户态切换到内核态,然后把数据读到内核缓冲区;
  3. 内核将数据拷贝到应用缓冲区,并切换回用户态,read()调用返回;
  4. JVM处理代码逻辑,然后执行write()系统调用;
  5. 操作系统切换到内核态,将数据从应用缓冲区拷贝到socket内核缓冲区;
  6. 操作系统返回到用户态,JVM继续执行后面的业务逻辑。

如果你的应用不关心延时和吞吐量等性能指标,那么以上做法是没问题的,但是如果你的应用有这方面要求,比如静态资源服务器,那么这样做将会无法满足性能要求。上图中有4次上下文切换以及2次不必要的内存拷贝。

系统级别的Zero-Copy(零拷贝)

从上面的方式中可以很清楚的看到,将数据从内核缓冲区拷贝到应用缓冲区,以及从应用缓冲区拷贝到socket内核缓冲区是完全没必要的,因为我们没有对数据作任何处理,仅仅只是将数据从一个socket倒腾到另一个socket。零拷贝技术就能消除这两次额外的内存拷贝。零拷贝技术的实现方式没有一个统一的标准,它取决于不同的操作系统。典型地,那些UNIX LIKE系统用sendfile()来实现零拷贝功能。
使用零拷贝方式实现上面场景的图示如下
在这里插入图片描述
你可能会说,操作系统还是要在内核内存空间做一次拷贝呀!是的。但是从操作系统的角度来说,它已经是零拷贝了,因为已经没有数据从内核空间拷贝到用户空间了。内核需要做一次拷贝的原因是一般的硬件DMA方式只能存取连续的内存空间(所以才有了缓冲区)。但是如果硬件支持scatter-n-gather特性,这次的拷贝就可以避免。
支持scatter-n-gather特性时的图示如下
在这里插入图片描述
很多WEB服务器都支持零拷贝,比如Tomcat和Apache。默认情况下Apache的这个特性是关闭的。
注意: Java的NIO通过transferTo方法提供零拷贝。

MMap

上面的零拷贝方案有个问题,因为没有涉及到用户态,所以除了打通流管道,我们无法通过代码来修改流管道里的数据。不过现在有个比零拷贝昂贵但优于传统I/O的方案——内存映射,简称MMap。
在这里插入图片描述
MMap允许代码将文件映射到内核内存,应用可以直接访问这个内核内存,就像访问用户态的内存空间一样,这样就不会产生内核空间到用户空间的内存拷贝。不过这种方式仍然需要4次上下文切换以及3次数据拷贝(其中有一次是CPU参与的内核内存拷贝)。操作系统将文件的某块数据映射到内存,受益于操作系统的虚拟内存管理,热点数据能被提前载入到内存,所有的数据是页对齐的,因此不需要缓冲区拷贝就能将数据倒腾到目标socket。

虽然,MMap避免了额外的内存拷贝,但是使用了MMap不一定会比普通的方式快,这取决于不同的操作系统。因为这涉及到MMap的创建和销毁所需要的性能开销以及页缺失时的负面影响。

Java中实现MMap方式的类是MappedByteBuffer,它其实也是一种DirectByteBufferDirectByteBufferMappedByteBuffer的子类),不过这两个类并没有直接的关系。我们通常所说的直接内存并没有MMap的特性。

DirectByteBuffer

Java NIO中有三种ByteBuffer

  1. HeapByteBufferByteBuffer.allocate()使用的就是这种缓冲区,叫堆缓冲区,因为它是在JVM堆内存的,支持GC和缓存优化。但是它不是页对齐的,也就是说如果要使用JNI的方式调用native代码时,JVM会先将它拷贝到页对齐的缓冲空间。
  2. DirectByteBufferByteBuffer.allocateDirect()方法被调用时,JVM使用C语言的malloc()方法分配堆外内存。由于不受JVM管理,这个内存空间是页对齐的且不支持GC,和native代码交互频繁时使用这种缓冲区能提高性能。不过内存分配和销毁的事就要靠你自己了。
  3. MappedByteBufferFileChannel.map()调用返回的就是这种缓冲区,这种缓冲区用的也是堆外内存,本质上其实就是对系统调用mmap()的封装,以便通过代码直接操纵映射物理内存数据。

参考资料

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我来简单地解释一下 Linux 中 DMA 拷贝数据到用户态的过程,以及零拷贝技术的实现。 首先,DMADirect Memory Access,直接内存访问)是一种计算机内部数据传输的技术,它可以在不占用 CPU 的情况下,直接将设备数据传输到内存中。在 Linux 中,DMA 可以通过一些 API(如 dma_alloc_coherent、dma_map_single 等)来进行操作。 在进行 DMA 拷贝数据到用户态时,通常需要经过以下步骤: 1. 首先,用户态程序需要申请一块内存空间,用来存放 DMA 传输的数据。 2. 接着,用户态程序需要调用一些 DMA 相关的 API,将这块内存空间映射到内核空间。这样,DMA 引擎就可以直接访问这块内存了。 3. 当 DMA 引擎完成数据传输后,它会触发一个中断,通知 CPU 数据已经传输完成。 4. 在中断处理程序中,CPU 可以将 DMA 传输的数据从内核空间拷贝到用户态程序申请的内存空间中。 接下来,我们来谈一下零拷贝技术。零拷贝技术是指在进行数据传输时,尽可能地减少 CPU 处理的次数,从而提高传输效率。在 Linux 中,零拷贝技术通常通过以下方式实现: 1. 利用 DMA 引擎直接将设备数据传输到内存中,不经过 CPU。 2. 利用 mmap 系统调用将内存映射到用户态程序的地址空间中。这样,用户态程序就可以直接访问内存中的数据,而不需要额外的拷贝操作。 3. 在传输完成后,用户态程序可以直接内存中的数据进行处理,而不需要将数据拷贝到另外一块内存中再进行处理。 通过这种方式,可以避免不必要的数据拷贝,提高数据传输的效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值