1.DMA技术详解
(1)应用程序 从 磁盘读写数据 的时序图(未用DMA技术前)
(2)什么是DMA 技术 (Direct Memory Access)
- 直接内存访问,直接内存访问是计算机科学中的一种内存访问技术。
- DMA之前:要把外设的数据读入内存或把内存的数据传送到外设,一般都要通过CPU控制完成,利用中断技术。
- 允许某些硬件系统能够独立于CPU直接读写操作系统的内存,不需要中处理器(CPU)介入处理。
- 数据传输操作在一个DM控制器(DMAC)的控制下进行,在传输过程中CPU可以继续进行其他的工作。
- 在大部分时间CPU和I/O操作都处于并行状态,系统的效率更高。
(3)应用程序的读写数据
- 读本地磁盘
- 操作系统检查内存缓冲区读取,如果存在则直接把内核空间的数据copy到用户空间(CPU负责),应用程序即可使用。
- 上步没数据,则从磁盘中读取到内核缓冲(DMA负责),再把内核空间的数据copy到用户空间(CPU负责),应用程序即可使用
- 硬盘->内核缓冲区->用户缓冲区
- 写操作本地磁盘
- 根据操作系统的写入方式不一样,buffer IO 和 direct IO ,写入磁盘时机不一样。
- buffer IO
- 应用程序把数据从用户空间copy到内核空间的缓冲区(CPU负责),再把内核缓冲区的数据写到磁盘(DMA负责)。
- direct IO
- 应用程序把数据直接从用户态地址空间写入到磁盘中,直接跳过内核空间缓冲区。
- 减少操作系统缓冲区和用户地址空间的拷贝次数,降低了CPU和内存开销。
- 用户缓冲区->内核缓冲区->硬盘
- 读网络数据
- 网卡Socket(类似磁盘)中读取客户端发送的数据到内核空间(DMA负责)。
- 把内核空间的数据copy到用户空间(CPU负责),然后应用程序即可使用。
- 写网络数据
- 用户缓冲区中的数据copy到内核缓冲区的Socket Buffer 中(CPU负责)
- 将内核空间中的Socket Buffer 拷贝到Socket协议栈(网卡设备)进行传输(DMA负责)
(4)DMA的工作总结
- 从磁盘的缓冲区到内核缓冲区的拷贝工作。
- 从网卡设备到内核的socket buffer 的拷贝工作。
- 从内核缓冲区到磁盘缓冲区的拷贝工作。
- 从内核的socket buffer到网卡设备的拷贝工作。
- 注意:内核缓冲区到用户缓冲区之间的拷贝工作仍然由CPU负责
(5)DMA技术带来的性能损耗
- 上图应用程序从磁盘读取数据发送到网络上的损耗,程序需要两个命令 先read读取,再write写出
- 四次内核态和用户态的切换
- 四次缓冲区的拷贝(2次DMA拷贝、2次CPU拷贝)
- 读取:磁盘缓冲区到内核缓冲区(DMA)
- 读取:内核缓冲区到用户缓冲区(CPU)
- 写出:用户缓冲区到内核缓冲区Socket Buffer(CPU)
- 写出:内核缓冲区的Socket Buffer到网卡设备(DMA)
为了解决这种性能的损耗所以就诞生了零拷贝。
2.ZeroCopy零拷贝技术简介
(1)什么是零拷贝ZeroCopy
减少不必要的内核缓冲区跟用户缓冲区之间的拷贝工作,从而减少CPU的开销和减少kernel和user模式的上下文切换,达到性能的提升。从磁盘中读取文件通过网络发送出去,只需要拷贝2\3次和2\4的内核态和用户态的切换即可。
ZeroCopy技术实现方式有两种(内核态和用户态切换次数不一样)
- 方式一:mmap+write
- 方式二:sendfile
(2)ZeroCopy的实现底层 mmap + write
- 操作系统都使用虚拟内存,虚拟地址通过多级页表映射物理地址。
- 多个虚拟内存可以指向同一个物理地址,虚拟内存的总空间远大于物理内存空间。
- 如果把内核空间和用户空间的虚拟地址映射到同一个物理地址,就不需要来回复制数据。
- mmap系统调用函数会直接把内核缓冲区的数据映射到用户空间,内核空间和用户空间就不需要在进行数据拷贝的操作了,节省了CPU开销。
- mmap()负责读取,write()负责写出
- 执行流程
- 应用程序先调用mmap()方法,将数据从磁盘拷贝到内核缓冲区,返回结束(DMA负责)。在调用write(),内核缓冲区的数据直接拷贝到内核socket buffer (CPU负责),然后把内核缓冲区的Socket Buffer 给直接拷贝给Socket协议线,即网卡设备中,返回结束(DMA负责)
- 采用mmap之后,CPU用户态和内核态上下文切换依旧是4次和全程有3次数据拷贝
- 2次DMA拷贝、1次CPU拷贝、4次内核态用户态切换,减少了1次CPU拷贝
(3)ZeroCopy的实现底层 sendfile
- Linux kernal 2.1新增发送文件的系统调用函数sendfile()。
- 执行流程
- 替代read()和write()两个系统调用,减少一次系统调用,即减少2次CPU上下文切换的开销,调用sendfile(),从磁盘读取到内核缓冲区,然后直接把内核缓冲区的数据拷贝到socket buffer缓冲区里,再把内核缓冲区的SocketBuffer给直接拷贝给Socket协议栈,即网卡设备中(DMA负责)。
- 采用sendfile后,CPU用户态和内核态上下文切换是2次 和 全程3次的数据拷贝,2次DMA拷贝、1次的CPU拷贝、2次内核态用户态切换。
- Linux 2.4+ 版本之后改进sendfile,利用DMA Gather(带有收集功能的DMA),变成了真正的零拷贝(没有CPU Copy)
- 应用程序先调用sendfile()方法,将数据从磁盘拷贝到内核缓冲区(DMA负责)
- 把内存地址、偏移量的缓冲区fd描述符拷贝到Socket Buffer中去 拷贝很少的数据,可忽略
- 本质和虚拟内存的解决方法思路一样,就是内存地址的记录
- 然后把内核缓冲区的Socket Buffer给直接拷贝给Socket协议栈 即网卡设备中,返回结束(DMA负责)
3.Java和主流中间件里的零拷贝技术
(1)Java中有哪些零拷贝技术
- Java NIO对mmap的实现 fileChannel.map()
- Java NIO对sendfile的实现 fileChannel.transferTo() 和 fileChannel.transferFrom()
(2)什么是FileChannel
- FileChannel是一个连接到文件的通道,可以通过文件通道读写文件,该常被用于搞笑的网络/文件的数据传输和大文件拷贝
- 应用程序使用FileChannel写完以后,数据是在PageCache上的,操作系统不定时的把PageCache的数据写入到磁盘。为了避免宕机数据丢失,使用channel.force(true) 把文件相关的数据强制刷入磁盘上去。
- 使用之前必须先打开它,但是无法直接new一个FileChannel。
- 常规通过使用一个InputStream、OutputStream或者RandomAccessFile来获取一个FileChannel实例。
(3)mmap方式实现
- map方法,把文件映射成内存映射文件
- MappedByteBuffer,是抽象类也是ByteBuffer的子类,具体实现子类是DirectByteBuffer,可被通道进行读写。
- 一次map大小要限制在2G内,过大map会增加虚拟内存回收和重新分配的压力,直接报错。
- FileChannel.java中的map对long size 进行了限制,不能大于Integer.MAX_VALUE,否则就报错
- JDK层做限制是因为底层C++的类型,无符号int类型最大是2^31 -1, 2^31 -1 字节就是 2GB - 1B。
(4)sendfile方式实现
-
fileChannel.transferTo(long postition,long count,WritableByteChannel target)
- 将字节从此通道的文件传输到给定的可写入字节通道。
- 返回值为真实拷贝的size,最大拷贝2G,超出2G的部分将丢弃。
-
fileChannel.transferFrom(ReadableByteChannel src, long position, long count)
- 将字节从给定的可读取字节通道传输到此通道的文件中
- 对比 从源通道读取并将内容写入此通道的循环语句相比,此方法更高效
- transferFrom允许将一个通道连接到另一个通道,不需要在用户态和内核态来回复制,同时通道的内核态数据也无需复制,transferTo只有源为FileChannel才支持transfer这种搞笑的复制方式,其他如SocketChannel都不支持transfer模式。
- 一般可以做FileChannel->FileChannel->FileChannel 和 FileChannel->SocketChannel的transfer零拷贝
4.文件IO性能对比实战
实现一个文件拷贝,对比不同IO方式性能差异,文件大小 200MB~5GB
编码实现:
- 普通java的io流
- 普通java的带buffer的io
- 零拷贝实现之mmap的io
- 零拷贝实现之sendfile的io
运行环境准备
- Linux CentOS7.X
- 安装JDK11 配置全局环境变量 vi /etc/profile
- 环境变量立刻生效
- source /etc/profile
- 查看安装情况 java -version
- 准备1.34G测试文件
(1)普通java的io验证
(2)普通java的带buffer的io
(3)零拷贝实现之mmap的io
- 一次 map 最大支持2GB,超过2GB会报错
(4)零拷贝实现之sendfile的io
- 最大拷贝2G,超出2G的部分将丢弃,最终拷贝的文件大小只有2GB多点,超过2GB可以考虑多次执行
(5)测试结果分析
- 1~2GB的文件
- 普通拷贝
- 普通java的io流【慢】3973924秒
- 普通java的带buffer的io【快】33196秒
- 零拷贝
- 零拷贝实现之mmap的io【快】7131秒
- 零拷贝实现之sendfile的io【快】1784秒
- 分析原因之前,我们先来了解一下局部性原理
- 普通的IO和Buffer IO,为什么带有Buffer的IO要比普通的IO性能高?
- 两种零拷贝的方式对比
5.主流中间件中用到的ZeroCopy技术
(1)Nginx使用的是sendfile 零拷贝
- WebServer处理静态页面请求时,是从磁盘中读取网页的内容,因为sendfile不能在应用程序中修改数据,所以最适合静态文件服务器或者是直接转发数据的代理服务器。
(2)rocketmq主要是mmap,也有小部分使用sendfile
- rocketMQ在消息存盘和网络发送使用mmap, 单个CommitLog文件大小默认1GB
- 要在用户进程内处理数据,然后再发送出去的话,用户空间和内核空间的数据传输就是不可避免的
(3)Kafka主要是sendfile,也有小部分使用mmap
- kafka 在客户端和 broker 进行数据传输时,broker 使用 sendfile 系统调用,类似 【FileChannel.transferTo】 API,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送,即 Linux 的 sendfile。
中读取网页的内容,因为sendfile不能在应用程序中修改数据,所以最适合静态文件服务器或者是直接转发数据的代理服务器。
(2)rocketmq主要是mmap,也有小部分使用sendfile
- rocketMQ在消息存盘和网络发送使用mmap, 单个CommitLog文件大小默认1GB
- 要在用户进程内处理数据,然后再发送出去的话,用户空间和内核空间的数据传输就是不可避免的
(3)Kafka主要是sendfile,也有小部分使用mmap
- kafka 在客户端和 broker 进行数据传输时,broker 使用 sendfile 系统调用,类似 【FileChannel.transferTo】 API,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送,即 Linux 的 sendfile。