mmap内存映射原理
mmap内存映射的实现过程,总的来说可以分为三个阶段:
- 创建虚拟空间:应用进程启动映射,在进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址作为映射区域;
- 建立地址映射:调用系统函数mmap(),创建页表,存储文件物理地址和进程虚拟地址的映射关系;
- 应用进程对映射区域进行读写;
mmap读流程
- 应用进程发起读请求,访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常;
- CPU接受到缺页中断后,向DMA控制器发出指令;
- DMA控制器开始将文件数据拷贝到物理内存;
- 拷贝完成后,更新页表,建立虚拟地址到物理内存的映射;
- 重新执行步骤1,获取到数据;
mmap写流程
- 应用进程发起写请求,访问虚拟地址空间这一段映射地址,通过页表查看是否存在对应的物理内存,如果不存在,则通过缺页异常加载对应的页;
- 页表中查询到对应物理内存后,直接写入数据;
- 用户主动触发刷盘,或者内核按照某种规则触发刷盘;
- DMA控制器将内核缓冲区数据写入磁盘;
适用/不适用场景
不适用场景
由于mmap()的实现很复杂,调用mmap()将会带来额外的开销,因此在一些情况下,没有使用mmap()的必要:
- 访问小文件时,直接使用read()或write()将更加高效;
- 单个进程对文件执行顺序访问时(sequential access),使用mmap()几乎不会带来性能上的提升。比如,使用read()顺序读取文件时,文件系统会使用 read-ahead 的方式提前将文件内容缓存到文件系统的缓冲区,因此使用read()将很大程度上可以命中缓存;
适用场景
- 对文件执行随机访问时,如果使用read()或write(),则意味着较低的 cache 命中率。这种情况下使用mmap()通常将更高效;
- 多个进程同时访问同一个文件时(无论是顺序访问还是随机访问),如果使用mmap(),那么 OS 缓冲区的文件内容可以在多个进程之间共享,从操作系统角度来看,使用mmap()可以大大节省内存。
Java应用
Java NIO中的FileChannel.map()方法是基于mmap()函数实现的,常用函数关系如下所示:
mmap注意点
- 对于大文件而言,内存映射比普通IO流要快,小文件则未必;
- 不要经常调用MappedByteBuffer.force()方法,这个方法强制操作系统将内存中的内容写入硬盘,所以如果你在每次写内存映射文件后都调用force()方法,你就不能真正从内存映射文件中获益,而是跟disk IO差不多。
- 读写内存映射文件是操作系统来负责的,因此,即使你的Java程序在写入内存后就挂掉了,只要操作系统工作正常,数据就会写入磁盘。
- 如果电源故障或者主机瘫痪,有可能内存映射文件还没有写入磁盘,意味着可能会丢失一些关键数据。
参考: