MMAP/Page Cache - 以Javaer的角度看待内存映射文件机制

666 阅读3分钟

什么是MMAP?

MMAP(Memory-mapped file)是一种内存映射机制。用于建立从文件到内存之间的一种映射,将对磁盘的写入映射为对内存的写入,以增加对于文件IO的效率。

在Linux上主要通过mmap()、munmap()、msync()等系统调用来实现。

什么是Page Cache?

Page Cache页缓存是Linux实现MMAP机制的底层关键组件,其将文件分为多个大小为4KB的数据块。

前面说文件传输过程中,将磁盘文件拷贝到内核缓冲区中,这个缓存区实际上就是Page Cache。

PageCache的大小在32位OS中为4K,在64位操作系统中大小为8K。 PageCache有两个特性:

  1. 缓存 在操作系统中,具有「局部性」,即刚被访问的数据再次被访问的概率更高。 所以Linux中使用Page Cache来缓存最近被访问的数据。所以读取磁盘的时候会有先从Page Cache中读取,如果没有再从磁盘中读取。

  2. 预读 PageCache的一个很好的特性就是预读,也就是当应用程序发生读操作时,文件系统会提前为应用程序读取的文件加载更多的数据到Page Cache中,这样下一次读取的时候就可以直接命中Page Cache了。(很多地方都运用到了这样的技术,比如说JVM中) 有效的减少了磁盘的访问次数。

在Java NIO中通过Channel.map()方法即可将文件区域映射到内存中实现MMAP机制,其会返回一个MappedByteBuffer对象。这个对象在Linux映射的内存区域实际上就是Page Cache。

FileChannel fileChannel =  new RandomAccessFile(new File("/tmpFile"), "rw").getChannel();  

MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024 * 4);

零拷贝的限制

  1. 不适合大文件,因为MMAP在加载的时候是预先加载内存中的空间的,如果映射文件太大就会造成占用过的内存空间。
  2. 不适合变长文件,因为MMAP在使用的时候需要指定映射文件的大小,一般为内存页的整数倍。
  3. 磁盘延迟,MMAP是通过缺页中断向磁盘发起真正的IO请求,而具体是何时写入将交由系统来决定,而不是应用程序。

RocketMQ中对零拷贝的优化

  1. 内存预映射机制 RocketMQ通过维护一个MappedFileQueue队列,在每次消息的写入都获取消息队列中的最后一个MappedFile,如果没有则会创建一个并且将下一个也创建好
  2. 文件预热 在调用mmap()进行内存映射时,其实只是建立起虚拟内存与物理地址的映射关系,不会加载任何文件到内存中。 而RocketMQ的在MappedFile#warmMappedFile方法中,每隔一个页(4k)就put一个0,将文件加载到内存中,这样当消息读取或者写入的时候可以直接命中Page Cache。
  3. mlock内存锁定 在warmMappedFile()预热后还会调用mlock方法, 将预热后的内存空间进行锁定,来防止操作系统将内存空间调到swap空间中。