RocketMQ源码级实现原理-内存映射

内存映射

MMAP 众所周知,基于 OS 的 mmap 的内存映射技术,通过 MMU 映射文件,使随机读写文件和读写内存相似的速度。

RocketMQ的MappedFile方式 写入 落盘
方式一 写入内存字节缓冲区,direct类型(writeBuffer) 从内存字节缓冲区(write buffer)提交(commit)到文件通道(fileChannel),fileChannel flush到磁盘

方式二 写入映射文件字节缓冲区(mappedByteBuffer) mappedByteBuffer force到磁盘
有意思的是方式一:数据写入到mmap出来的mappedByteBuffer,而是写到一个DirectBuffer里面,然后commit到fileChannel里面,再由fileChannel刷盘

69c6cfa1607b4b32a596879e20191f57.png

MappedFile

rocketmq中,每个mappedFile对应的磁盘文件初始化出来就是1G的大小,初始化出来后它里面会有很多的占位符。

参考链接:

什么是 “零拷贝” ? - SpringForAll - 博客园

RocketMQ原理详解——零拷贝机制_wukurua的博客-CSDN博客_rocketmq 零拷贝

java实践4之浅谈文件IO、实现带缓冲区读写、NIO、理解Bytebuffer核心参数、Mmap、零拷贝_马大帅_的博客-CSDN博客_allocatedirect()和mmap区别

什么是Mmap

底层实现就是将虚拟地址空间和文件,通过页表建立起映射。然后程序员在代码中通过操作虚拟地址,就可以实现对磁盘文件的读写操作。

实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

从而:

有了mmap内存映射,则虚拟地址空间和内核缓冲区空间(Pagecache),就连在了一起。通过mmap()函数返回的虚拟地址空间指针,操作虚拟地址空间那么同时Pagecache也就实时同步感受到了变化。不像原来普通IO,操作修改了虚拟地址空间后,还要继续把虚拟地址空间中的内容,再通过系统调用write()给写到Pagecache空间中去。

a652818a712c4ea186e4eeb87c72a176.png

length映射区大小,表示要将文件中多大的部分对应到内存

使用mmap需要注意的一个关键点是,mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。

案例:

841d7b8700f74f8184d798401f9625ed.png

可以看到第33行,就在使用指针的方式,来操作这片虚拟内存,因为是mmap映射下的这片内存,所以操作了这片虚拟地址空间,那么对应的磁盘文件也会被修改。

输出结果:

4075350356e742fab349fb05fa32095a.png36行代码,是释放内存映射的这片虚拟地址空间(删除这段内存映射,也就是从页表中把这段映射删除),否则当前进程的别的线程,就不能再使用这片虚拟地址空间了。

Java nio肯定比Java io要快,主要因为java nio是基于block的传输,将数据聚合(cache)成block再写到磁盘(或者读取),速度自然比基于字节的流要快。

MappedFileBuffer与FileChannel的关系:

Mmap出来的MapperByteBuffer会作为page cache的一部部分

FileChannel结合DirectBuffer会提高写入性能,避免了堆内存的一次拷贝????
FileChannel写DirectBuffer之后,数据就到了PageCache里面,或者自己sync,或者依赖操作系统的刷新策略
FileChannel写DirectBuffer之后,数据就到了PageCache里面,就到了MapperByteBuffer里面,因为MapperByteBuffer被用作了文件的cache

1)MappedByteBuffer使用虚拟内存,因此分配(map)的内存大小不受JVM的-Xmx参数限制,但是也是有大小限制的。

2)如果当文件超出1.5G限制时,可以通过position参数重新map文件后面的内容;

3)MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。

Mmap方式读写文件和普通IO读写文件的区别

java io操作中通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高。

  1. 通过java.nio包和MappedByteBuffer允许Java程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得IO操作非常快。加载内存映射文件所使用的内存在Java堆区之外。
  2. 在传统的文件IO操作中,我们都是调用操作系统提供的底层标准IO系统调用函数 read()、write() ,此时调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。这么做是为了减少磁盘的IO操作,为了提高性能而考虑的,因为我们的程序访问一般都带有局部性,也就是所谓的局部性原理,在这里主要是指的空间局部性,即我们访问了文件的某一段数据,那么接下去很可能还会访问接下去的一段数据,由于磁盘IO操作的速度比直接 访问内存慢了好几个数量级,所以OS根据局部性原理会在一次read()系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中,当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程私有空间,避免了再次的低 效率磁盘IO操作。

总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制(PageCache)。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

c27f43586ed711a640a77c4c3e4d2c4b.png

缺页错误出现的原因,不同类型的Page Fault出现的原因也不一样,常见的几种原因包括:

非法操作访问越界 这种情况产生的影响也是最大的,也是Coredump的重要来源,比如空指针解引用或者权限问题等都会出现缺页错误。
使用malloc新申请内存 malloc机制是延时分配内存,当使用malloc申请内存时并未真实分配物理内存,等到真正开始使用malloc申请的物理内存时发现没有才会启动申请,期间就会出现Page Fault。
访问数据被swap换出 物理内存是有限资源,当运行很多进程时并不是每个进程都活跃,对此OS会启动内存页面置换将长时间未使用的物理内存页帧放到swap分区来腾空资源给其他进程,当存在于swap分区的页面被访问时就会触发Page Fault从而再置换回物理内存。
=========================================================================

触发Page Fault的原因可能有很多,归根到底也只有几种大类:

1. 如使用共享内存区域,没有存储VA->PA(虚拟内存到物理内存)的映射但是存在物理页帧的软缺页错误,在Page Table/TLB中建立映射关系即可。

2. 访问的地址在物理内存中确实不存在,需要从磁盘/swap分区读入才能使用,这种性能影响会比较大,因为磁盘太慢了,尽量使用高性能的SSD来降低延时。

3. 访问的地址内存非法,缺页错误会升级触发SIGSEGV信号结束进程,这种属于可以导致进程挂掉的一种缺页错误。

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值