零拷贝
# 传统数据读取方式
图1
图2
调用
read()函数引起了一次上下文切换,由用户态切换为内核态,在系统内部调用了sys_read()函数开始从文件中读取数据。第一次 COPY 发生在由 DMA 从外设(硬盘、显卡、网卡等)读取数据到内核空间。请求的数据在内核空间的缓冲区准备好了之后开始把数据 COPY 到用户空间,
read()函数返回数据,引起了第二次的上下文切换,由内核态切换到用户态。数据处理完之后,开始调用
send()函数,此时产生了第三次的上下文切换,由用户态切换到内核态。第三次 COPY 是把应用数据放回到内核空间的缓冲区,由 CPU 再把数据 COPY 到内核空间。send()函数返回时发生了第四次的上下文切换。DMA 会异步的把内核空间的缓冲区数据 COPY 到相关的外设上。
Java 中实现了两种零拷贝的方式,一种是 MMAP (Memory Mapped Files),另一种是 TransferTo (sendfile)
# MMAP 方式
Java 通过在堆外内存开辟一块共享空间,把硬盘上的数据和共享内存地址做一个映射关系,数据不再需要 COPY 到用户空间操作,应用程序操作完内存数据之后,直接由 CPU COPY 把数据 COPY 到对应进程缓存,减少了数据在内核空间和用户空间之间数据的一次 COPY。
发起 mmap 系统调用,产生一次上下文切换,由用户态切换到内核态。第一次 COPY 发生在由 DMA 从外设(硬盘、显卡、网卡等)读取数据到内核空间缓冲区。
mmap 系统调用返回,产生第二次上下文切换,由内核态切换到用户态。此后用户空间和内核空间共享缓冲区数据,用户空间就可以像操作自己缓冲区数据一样操作共享缓冲区数据。
发起 write 调用,产生第三次上下文切换。将共享缓冲区的数据由 CPU COPY 到外设对应的缓冲区。
最后一步,write 函数返回,产生了第四次上下文切换。DMA 会异步的把内核空间的缓冲区数据 COPY 到相关的外设上。
与传统的数据读取方式相比,MMAP 少了一次 CPU COPY ,应用程序在操作数据事,不需要把数据由内核空间 COPY 到用户空间,而是映射出一块内存区域,直接操作共享区域的内存即可,提高了效率。
MappedByteBuffrer 使用的是堆外内存,因此分配内存大小不受 JVM 配置参数 -Xmx 的限制,主要受限于物理内存大小。因此配置 -Xmx 大小时不要配置物理最大内存,预留一部分给堆外内存使用。
MappedByteBuffer 提供了文件映射内存的
mmap方法,也提供了是放映射内存的unmap方法,然而unmap是FileChannelImpl中的私有方法,无法直接调用。(不建议手动释放)。
# transferTo 方式
transferTo() 方法把数据流从一个 channel 直接转移到指定的另一个 channel 中,依赖于操作系统的实现,在 unix 系统中调用了 sendfile() 系统方法来实现。上下文切换由 4 次减少到 2 次,数据 COPY 由 4 次变为 3 次。
# 数据 copy
# 系统调用
- 用户端调用
transferTo()方法之后文件内容通过 DMA COPY 到 内核的输入缓冲区 中去 - 数据通过内核调用被 COPY 到与输出套接字关联的内核缓冲区中
- DMA 将数据从内核缓冲区中 COPY 到对应的外设中
但是以上的调用并未实现真正零拷贝 的目标,如果底层网络接口支持 gather 操作,我们可以进一步减少内核的重复操作。在 Linux 内核 2.4 版本之后,出现了套接字缓冲区描述符 ( socket buffer descriptor)解决了这个问题。这种方法不仅减少了上下文的切换,还消除了 CPU 参与数据拷贝的过程(通过描述符直接操作同一块缓冲区数据)。用户端调用方法不变,内核级实现。优化之后的操作:
- 调用
transferTo()函数之后由 DMA 触发把数据 COPY 到一块内核缓冲区中 - 数据集不再需要被复制到 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:
提供统一数据接口。外接设备通过各自协议接入操作系统,操作系统统一封装数据,对应用程序提供统一数据接口。
数据安全性。应用程序只需要跟操作系统提供的函数打交道,不去跟硬件直接进行数据交换。
操作系统统一调度。由操作系统统一调度对某硬件接口的任务管理。
# Q: MMAP 内存映射文件的内存什么时候会被回收?
虽然提供了 munmap 方法,但是 JDK 并没有直接开放(私有方法),虽然可以通过反射实现调用,但是不推荐。因为如果用户主动调用,会导致 GC DirectBuffer 的时候,报出内存访问异常导致 JVM 崩溃 (如果用户调用了munmap,对应的MappedByteBuffer被GC时,会在被调用一次。这时如果内存已经被其他程序占用,会报一个内存访问异常)。
想解除映射只能先把 buffer 置为 null,然后等待 GC 赶紧起作用。所以,在写完当前文件块,需要映射下一块文件时,我们一般就把对应的 MappedByteBuffer 设置为 null 就行了,然后继续 map 就行了。如果内存不足,会自动触发 System.gc()。