零拷贝(Mmap,sendfile)
2024-08-28 14:16:24 0 举报
AI智能生成
零拷贝是一种高效的数据传输技术,通过在内核空间中直接进行数据传输,避免了数据在应用程序地址空间和内核地址空间之间的来回复制。其中,Mmap(内存映射)和sendfile(发送文件)是实现零拷贝技术的两种主要方式。Mmap通过将文件或设备映射到内存中,避免数据从内核空间到用户空间的复制。sendfile则允许将数据直接从一个文件描述符传输到另一个文件描述符,从而省去了用户空间和内核空间之间的数据复制。这两种技术大大提高了数据传输的效率,减少了系统资源的消耗。
作者其他创作
大纲/内容
所谓的零拷贝技术,其实并不是不拷贝,而是要尽量减少CPU拷贝
操作系统对于内存空间,是分为用户态和内核态的。用户态的应用程序无法直接操作硬件,需要通过内核空间进行操作转换,才能真正操作硬件。这其实是为了保护操作系统的安全。正因为如此,应用程序需要与网卡、磁盘等硬件进行数据交互时,就需要在用户态和内核态之间来回的复制数据。而这些操作,原本都是需要由CPU来进行任务的分配、调度等管理步骤的,早先这些IO接口都是由CPU独立负责,所以当发生大规模的数据读写操作时,CPU的占用率会非常高。
DMA拷贝极大的释放了CPU的性能,因此他的拷贝速度会比CPU拷贝要快很多。但是,其实DMA拷贝本身,也在不断优化。
引入DMA拷贝之后,在读写请求的过程中,CPU不再需要参与具体的工作,DMA可以独立完成数据在系统内部的复制。但是,数据复制过程中,依然需要借助数据总进线。当系统内的IO操作过多时,还是会占用过多的数据总线,造成总线冲突,最终还是会影响数据读写性能。
之后,操作系统为了避免CPU完全被各种IO调用给占用,引入了DMA(直接存储器存储)。由DMA来负责这些频繁的IO操作。DMA是一套独立的指令集,不会占用CPU的计算资源。这样,CPU就不需要参与具体的数据复制的工作,只需要管理DMA的权限即可
这也解释了,为什么Java应用层与零拷贝相关的操作都是通过Channel的子类实现的。这其实是借鉴了操作系统中的概念。
为了避免DMA总线冲突对性能的影响,后来又引入了Channel通道的方式。Channel,是一个完全独立的处理器,专门负责IO操作。既然是处理器,Channel就有自己的IO指令,与CPU无关,他也更适合大型的IO操作,性能更高。
CPU拷贝 和 DMA 拷贝
如果没有,就必须从磁盘中读取数据,然后内核将读取的数据再缓存到cache中,如此后续的读请求就可以命中缓存了
font color=\"#e74f4c\
page可以只缓存一个文件的部分内容,而不需要把整个文件都缓存进来
读Cache
当内核发起一个写请求时,也是直接往cache中写入,后备存储中的内容不会直接更新。
内核会将被写入的page标记为`dirty`, 并将其加入到`dirty list`中。
内核会周期性地将dirty list中的page写回到磁盘上, 从而使磁盘上的数据和内存中缓存的数据 一致。
写Cache
Cahce回收
Page Cache(类mysql BufferPool)
缓存区,是高速缓存,是位于CPU和主内存之间的容量较小但速度很快的存储器,因为CPU的速度远远高于主内存的速度,CPU从内存中读取数据需等待很长的时间,而 Cache 保存着CPU刚用过的数据或循环使用的部分数据,这时从Cache中读取数据会更快,减少了 CPU等待的时间,提高了系统的性能。Cache并不是缓存文件的,而是缓存块的(块是I/O读写最小的单元);Cache一般会用在I/O请求上, 如果多个进程要访问某个文件,可以把此文件读入Cache中,这样下一个进程获取CPU控制权并访问此 文件直接从Cache读取,提高系统性能。
Cache
缓冲区,用于存储速度不同步的设备或优先级不同的设备之间传输数据;通过buffer 可以减少进程间通信需要等待的时间,当存储速度快的设备与存储速度慢的设备进行通信时, 存储慢的数据先把数据存放到buffer,达到一定程度存储快的设备再读取buffer的数据,在此 期间存储快的设备CPU可以干其他的事情。Buffer:一般是用在写入磁盘的,例如:某个进程要求多个字段被读入,当所有要求的字段被读入之前已经读入的字段会先放到buffer中。
Buffer
cache和buffer的区别
是在jvm堆上面一个buffer,底层的本质是一个数组,用类封装维护了很多的索引(`limit/position/capacity`等)
优点:内容维护在jvm里,把内容写进buffer里速度快;更容易回收。
分配效率高,读写效率较低
HeapByteBuffer
底层的数据是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向数据,进而操作数据。
外设之所以要把jvm堆里的数据copy出来再操作,不是因为操作系统不能直接操作jvm内存,而是因为jvm在进行gc(垃圾回收)时,会对数据进行移动,一旦出现这种问题,外设就会出现数据错乱的情况
优点:跟外设(IO设备)打交道时会快很多,因为外设读取jvm堆里的数据时, 不是直接读取的,而是把jvm里的数据读到一个内存块里,再在这个块里读取的,如果使用 DirectByteBuffer,则可以省去这一步,实现zero copy(零拷贝)所有的通过allocate方法创建的buffer都是HeapByteBuffer
java里面的直接内存是操作 Unsafe 类来完成的操作
分配效率低,读写效率高
1. 前者分配在JVM堆上(ByteBuffer.allocate()),后者分配在操作系统物理内存上 (`ByteBuffer.allocateDirect()`,JVM使用C库中的`malloc()`方法分配堆外内存);2. DirectByteBuffer可以减少JVM GC压力,当然,堆中依然保存对象引用,fullgc发生时也会回收直接内存,也可以通过`system.gc`主动通知JVM回收,或者通过 cleaner.clean主动清理。 Cleaner.create()方法需要传入一个DirectByteBuffer对象和一个Deallocator(一个堆外内存回收线程)。GC发生时发现堆中的DirectByteBuffer对象没有强引用了,则调用Deallocator 的run()方法回收直接内存,并释放堆中DirectByteBuffer的对象引用;3. 底层I/O操作需要连续的内存(JVM堆内存容易发生GC和对象移动),所以在执行write操作时 需要将HeapByteBuffer数据拷贝到一个临时的(操作系统用户态)内存空间中,会多一次额外拷贝。而DirectByteBuffer则可以省去这个拷贝动作,这是Java层面的 “零拷贝” 技术,在 netty中广泛使用;4. MappedByteBuffer底层使用了操作系统的mmap机制,FileChannel#map()方法就会返回 MappedByteBuffer。DirectByteBuffer虽然实现了MappedByteBuffer,不过 DirectByteBuffer默认并没有直接使用mmap机制。`
堆外内存实现零拷贝
DirectByteBuffer
HeapByteBuffer 和 DirectByteBuffer
缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制 中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说
1. 在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;
2. 可以减少读盘的次数,从而提高性能。
缓存I/O的优点
在缓存 I/O 机制中,`DMA` 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输。数据在传输过程中就需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作, 这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
缓存I/O的缺点
缓存IO
直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种 更加有效的缓存机制来提高数据库中数据的存取性能(例如:Mysql的 BufferPool)
如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常缓慢。通常直接IO与异步IO结合使用,会得到比较好的性能。
直接IO的缺点
直接IO
下图分析了写场景下的DirectIO和BufferIO:
缓冲IO和直接IO
在LINUX中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。
映射关系可以分为两种:文件映射:磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。匿名映射 :初始化全为0的内存空间。
映射关系是否共享又分为私有映射(MAP_PRIVATE): 多进程间数据共享,修改不反应到磁盘实际文件,是一个copy-onwrite(写时复制)的映射方式。共享映射(MAP_SHARED): 多进程间数据共享,修改反应到磁盘实际文件中。
在mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生\"缺页\",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096【4k】)加载到物理内存,注意是只加载缺 页,但也会受操作系统一些调度策略影响,加载的比所需的多。
mmap只是在虚拟内存分配了地址空间,只有在第一次访问虚拟内存的时候才分配物理内存
内存映射文件(Mmap)
私有文件映射:多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中
私有匿名映射:mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存 (malloc分配大内存会调用mmap)。 例如开辟新进程时,会为每个进程分配虚拟的地址空间, 这些虚拟地址映射的物理内存空间各个进程间读的时候共享,写的时候会copy-on-write。
共享文件映射:多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件 的修改会反 应到实际物理文件中,他也是进程间通信(IPC)的一种机制。
共享匿名映射:这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理 内存页,这也就实现了父子进程通信(IPC).
总结起来有4种组合
直接内存读取并发送文件的过程
这个拷贝过程都是在操作系统的系统调用层面完成的,在Java应用层,其实是无法直接观测到的,但是我们可以去JDK源码当中进行间接验证。在JDK的NIO包中,`java.nio.HeapByteBuffer`映射的就是JVM的一块堆内内存,在HeapByteBuffer中,会由一个byte数组来缓存数据内容,所有的读写操作也是先操作这个byte数组。这其实就是没有使用零拷贝的普通文件读写机制。
NIO把包中的另一个实现类`java.nio.DirectByteBuffer`则映射的是一块堆外内存。在DirectByteBuffer中,并没有一个数据结构来保存数据内容,只保存了一个内存地址。所有对数据的读写操作,都通过unsafe魔法类直接交由内核完成,这其实就是mmap的读写机制。
mmap的映射机制由于还是需要用户态保存文件的映射信息,数据复制的过程也需要用户态的参与,这其中的变数还是非常多的。所以,mmap机制适合操作小文件,如果文件太大,映射信息也会过大,容易造成很多问题。通常mmap机制建议的映射文件大小不要超过2G 。而RocketMQ做大的CommitLog文件保持在1G固定大小,也是为了方便文件映射。
Mmap读取并发送文件的过程
主要是通过`java.nio.channels.FileChannel#transferTo`方法完成font color=\"#ed9745\
还记得Kafka当中是如何使用零拷贝的吗?你应该看到过这样的例子,就是Kafka将文件从磁盘复制到网卡时,就大量的使用了零拷贝。百度去搜索一下零拷贝,铺天盖地的也都是拿这个场景在举例。
早期的 sendfile实现机制其实还是依靠CPU进行页缓存与socket缓存区之间的数据拷贝。但是,在后期的不断改进过程中,sendfile优化了实现机制,在拷贝过程中,并不直接拷贝文件的内容,而是只拷贝一个带有文件位置和长度等信息的文件描述符FD,这样就大大减少了需要传递的数据。而真实的数据内容,会交由DMA控制器,从页缓存中打包异步发送到socket中
sendfile机制在内核态直接完成了数据的复制,不需要用户态的参与,所以这种机制的传输效率是非常稳定的。sendfile机制非常适合大数据的复制转移。
Sendfile零拷贝读取并发送文件的过程
1. fileInputStream.read(buffer) 操作数据拷贝及状态转换分析
硬盘文件 -> 内核缓冲区 ( 内核空间 ) -> 用户缓冲区 ( 用户空间 ) -> Socket 缓冲区 ( 内核空间 ) -> 协议栈
传统IO拷贝次数分析
用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态
传统IO状态改变分析
4次数据拷贝,3次用户态和内核态的切换【最终如果算上将内核态切换为用户态(执行应用代码逻辑),则为4次】
传统IO
硬盘文件 -> 内核缓冲区 ( 内核空间 ) -> Socket 缓冲区 ( 内核空间 ) -> 协议栈
mmap 数据拷贝过程
mmap 状态切换
DirectByteBuf 直接内存(Mmap) 【3次copy,3(4)次切换】
数据拷贝分析
用户态 -> 内核态 -> 用户态
sendFile 函数 状态切换分析
【3次copy ,1(2)次切换】 进一步优化(底层采用了 linux 2.1 后提供的 `sendFile` 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据
'
硬盘文件 -> 内核缓冲区 ( 内核空间 ) -> 协议栈
NIO优化
优点
零拷贝
mmap 用于文件共享,很少用于socket操作,sendfile用于发送文件.
mmap 适合小数据量读写,sendFile 适合大文件传输。
mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 2 次上下文切换,最少 2 次数据拷贝。
sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)
mmap与sendFile区别
mmap是共享一个文件,共享内存是共享一段内存。mmap还可以写回到file
mmap和共享内存的区别
mmap缺点
优点:即使频繁调用,使用小文件块传输,效率也很高缺点:不能很好的利用DMA方式,会比sendfile多消耗CPU资源,内存安全性控制复杂,需要避免JVM Crash问题
使用mmap+write方式
优点:可以利用DMA方式,消耗CPU资源少,大块文件传输效率高,无内存安全问题缺点:小块文件效率低于mmap方式,只能是BIO方式传输,不能使用NIO
使用sendfile方式
rocketMQ 在消费消息时,使用了 mmap,因为小块数据传输比sendFile好。kafka 使用了 sendFile
总结
0 条评论
回复 删除
下一页