目录

零拷贝

# 传统数据读取方式

图1

figure1.gif

图2

figure2.gif
  1. 调用 read() 函数引起了一次上下文切换,由用户态切换为内核态,在系统内部调用了 sys_read() 函数开始从文件中读取数据。第一次 COPY 发生在由 DMA 从外设(硬盘、显卡、网卡等)读取数据到内核空间。

  2. 请求的数据在内核空间的缓冲区准备好了之后开始把数据 COPY 到用户空间,read() 函数返回数据,引起了第二次的上下文切换,由内核态切换到用户态。

  3. 数据处理完之后,开始调用 send() 函数,此时产生了第三次的上下文切换,由用户态切换到内核态。第三次 COPY 是把应用数据放回到内核空间的缓冲区,由 CPU 再把数据 COPY 到内核空间。

  4. send() 函数返回时发生了第四次的上下文切换。DMA 会异步的把内核空间的缓冲区数据 COPY 到相关的外设上。

Java 中实现了两种零拷贝的方式,一种是 MMAP (Memory Mapped Files),另一种是 TransferTo (sendfile)

# MMAP 方式

Java 通过在堆外内存开辟一块共享空间,把硬盘上的数据和共享内存地址做一个映射关系,数据不再需要 COPY 到用户空间操作,应用程序操作完内存数据之后,直接由 CPU COPY 把数据 COPY 到对应进程缓存,减少了数据在内核空间和用户空间之间数据的一次 COPY。

微信截图_20220522202334.png
  1. 发起 mmap 系统调用,产生一次上下文切换,由用户态切换到内核态。第一次 COPY 发生在由 DMA 从外设(硬盘、显卡、网卡等)读取数据到内核空间缓冲区。

  2. mmap 系统调用返回,产生第二次上下文切换,由内核态切换到用户态。此后用户空间和内核空间共享缓冲区数据,用户空间就可以像操作自己缓冲区数据一样操作共享缓冲区数据。

  3. 发起 write 调用,产生第三次上下文切换。将共享缓冲区的数据由 CPU COPY 到外设对应的缓冲区。

  4. 最后一步,write 函数返回,产生了第四次上下文切换。DMA 会异步的把内核空间的缓冲区数据 COPY 到相关的外设上。

与传统的数据读取方式相比,MMAP 少了一次 CPU COPY ,应用程序在操作数据事,不需要把数据由内核空间 COPY 到用户空间,而是映射出一块内存区域,直接操作共享区域的内存即可,提高了效率。

  • MappedByteBuffrer 使用的是堆外内存,因此分配内存大小不受 JVM 配置参数 -Xmx 的限制,主要受限于物理内存大小。因此配置 -Xmx 大小时不要配置物理最大内存,预留一部分给堆外内存使用。

  • MappedByteBuffer 提供了文件映射内存的 mmap 方法,也提供了是放映射内存的 unmap 方法,然而 unmapFileChannelImpl 中的私有方法,无法直接调用。(不建议手动释放)。

# transferTo 方式

transferTo() 方法把数据流从一个 channel 直接转移到指定的另一个 channel 中,依赖于操作系统的实现,在 unix 系统中调用了 sendfile() 系统方法来实现。上下文切换由 4 次减少到 2 次,数据 COPY 由 4 次变为 3 次。

# 数据 copy

图3

# 系统调用

figure4

  1. 用户端调用 transferTo() 方法之后文件内容通过 DMA COPY 到 内核的输入缓冲区 中去
  2. 数据通过内核调用被 COPY 到与输出套接字关联的内核缓冲区中
  3. DMA 将数据从内核缓冲区中 COPY 到对应的外设中

但是以上的调用并未实现真正零拷贝 的目标,如果底层网络接口支持 gather 操作,我们可以进一步减少内核的重复操作。在 Linux 内核 2.4 版本之后,出现了套接字缓冲区描述符 ( socket buffer descriptor)解决了这个问题。这种方法不仅减少了上下文的切换,还消除了 CPU 参与数据拷贝的过程(通过描述符直接操作同一块缓冲区数据)。用户端调用方法不变,内核级实现。优化之后的操作:

figure5

  1. 调用 transferTo() 函数之后由 DMA 触发把数据 COPY 到一块内核缓冲区中
  2. 数据集不再需要被复制到 socket 缓冲区。直接使用描述符和来操作内核缓冲区数据,最后由 DMA 把缓冲区数据写入到指定的协议单元中去。这种方法最终取消掉了 CPU 的复制过程。

零拷贝技术的原理与在java中应用_morris131的博客-CSDN博客_java 零拷贝技术 (opens new window)

Java 两种zero-copy零拷贝技术mmap和sendfile的介绍 - 掘金 (opens new window)

IBM Developer (opens new window)

# Q & A

# Q: CPU 怎么调度的 DMA ?同步执行还是异步执行?
#
# Q:为什么要由数据不是直接给到应用程序?为啥还要把数据由内核空间转移到用户空间?
# A:
  1. 提供统一数据接口。外接设备通过各自协议接入操作系统,操作系统统一封装数据,对应用程序提供统一数据接口。

  2. 数据安全性。应用程序只需要跟操作系统提供的函数打交道,不去跟硬件直接进行数据交换。

  3. 操作系统统一调度。由操作系统统一调度对某硬件接口的任务管理。

# Q: MMAP 内存映射文件的内存什么时候会被回收?

虽然提供了 munmap 方法,但是 JDK 并没有直接开放(私有方法),虽然可以通过反射实现调用,但是不推荐。因为如果用户主动调用,会导致 GC DirectBuffer 的时候,报出内存访问异常导致 JVM 崩溃 (如果用户调用了munmap,对应的MappedByteBuffer被GC时,会在被调用一次。这时如果内存已经被其他程序占用,会报一个内存访问异常)。

想解除映射只能先把 buffer 置为 null,然后等待 GC 赶紧起作用。所以,在写完当前文件块,需要映射下一块文件时,我们一般就把对应的 MappedByteBuffer 设置为 null 就行了,然后继续 map 就行了。如果内存不足,会自动触发 System.gc()。

上次更新: 2024/11/05, 03:15:29