mmap的简单原理解析

mmap很早就知道一些,但是只是简单知道这个可以减少内存消耗,并不知道它的实现原理是怎么样的,所以也不知道它到底是怎么去减少内存消耗的,然后找了相关资料看了下,发现和android的bundle机制还有点相似,这里做个记录和总结

一些概念

我们都知道linux进程分为用户空间内核空间,后来在零拷贝原理中知道对于32位操作系统来说系统会给每个应用进程分配4G的虚拟内存空间,其中0-3G的内存地址属于用户空间,3-4G的内存地址属于内核空间。其中用户空间是不能共享的,而内核空间是运行操作系统的,它独立于普通的应用程序,是被所有应用程序共享的。所以对一个应用来说,只有3G的用户空间是属于应用本身的。参考:内核空间与用户空间。由于用户空间是隔离的,所以这里当一个进程想访问另外一个进程的用户数据时就涉及到进程之间的通讯了(RPC),Android使用bundle来更好的解决这个问题,后面单独写Android的bundle机制。

mmap是什么

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系。

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

mmap的简单原理

传统的读写文件

一般来说,修改一个文件需要三个步骤

  • 把文件内容读入到内存中
  • 修改内存中的内容
  • 把内存的数据写入到文件中

过程如下图:

从上图可以看到磁盘中的文件file要被用户修改的时候,需要先把file文件拷贝到页缓存(page cache)中,然后通过系统函数read读到用户空间的临时buffer中(copy),修改之后,再通过write方法(copy)写回页缓存中,最终改变了磁盘中的file的文件。其中,页缓存是读写文件的中间层,内核使用页缓存与文件的数据块(连续的内存地址)关联起来。所以应用程序读写文件时,实际操作的是页缓存。这里为了实现文件修改在内存中是做了两次copy的。

使用mmap读写文件

从上面读写文件的过程中,我们可以看到有一个地方是可以优化的:如果可以直接在用户空间操作(读写)页缓存,那么久可以免去页缓存的数据复制到用户空间的临时buffer的过程。mmap其实就是做这个事情的。

使用mmap系统调用可以将用户空间的虚拟内存地址与文件进行映射(怎么做到的?),对映射的虚拟内存地址就行读写操作就如同对文件进行读写操作一下,如图:

mmap最终是调用了sendfile方法来实现映射关系的(具体怎么做到将用户空间的内存和页缓存做映射的,可以看这篇文章:认证分析mmap,这里不深入写,只要知道mmap的第一层原理,它减少了将内核空间往用户空间拷贝的动作,所以性能会很好。)

由于读写文件都需要经过页缓存,所以mmap映射的正是文件的页缓存,而非磁盘中的文件本身。由于mmap映射的是文件的页缓存,所以就涉及到同步的问题:页缓存上面时候把数据同步到磁盘?linux内核并不会主动把mmap映射的页缓存同步到磁盘,而是需要用户主动触发。同步mmap映射的内存到磁盘有4个时机:

  • 调用msync函数主动进行数据同步(主动)
  • 调用munmap函数对文件进行解除映射关系时(主动)
  • 进程推出时(被动)
  • 系统关机时(被动)

mmap 函数会返回映射后的内存地址,我们可以通过此内存地址对文件进行读写操作。如图