mmap 经常出现在对性能有要求的模块,Android系统中特有的进程间通信binder机制核心原理也是基于其实现,本文旨在以一种相对浅显易懂的方式来理清mmap的作用和高效的原因。

虚拟地址空间

早期操作系统运行程序时是直接将整个程序加载到物理内存,并且程序内访问到的内存地址也是真实的物理地址。这就导致了一些严重的问题,例如程序(进程)间的内存可以相互访问导致安全问题、内存的随机划分导致程序每次运行的实际地址不确定等等。

为了解决这些问题,虚拟地址空间的概念被提出。进程访问到的内存地址将不再是真实的物理地址,取而代之的是一组逻辑上的连续地址空间,并且对每个进程都一样,操作系统内部通过MMU负责将物理内存地址与虚拟地址建立映射关系,从进程角度来看,它可以访问一组地址固定且连续的内存空间,至于这块地址到底是被映射至RAM的哪一块、或者来自磁盘还是其它内存缓存是不知道的,因此也就不会错误或有意访问到其它进程的内存地址。

此外,一次性将整个程序(文件)加载到内存也会使得可运行的程序个数有限,内存无法得到很好的利用。实际上大部分情况在小段时间内只会用到文件的少部分数据,对此的解决方案就是分页,并采用以页为单位的页缓存技术。地址空间逻辑上被划分为了很多页,进程需要访问某个文件时,通过内核从磁盘上加载到内存都是按页加载,需要用到哪一页就加载并缓存相关页及其相邻页。

内核/用户空间

上面说到进程加载文件需要通过内核来实现,这是操作系统从安全(例如防止因为某个进程调用了不当指令就造成整个系统崩溃)角度考虑和设计的。普通用户进程操作和系统资源请求被区分开来,直接负责系统资源请求的部分由操作系统的内核实现,内存划分了一部分专门给内核使用,作为内核缓冲区,规定只有内核才有对底层硬件设备的所有控制权限,可以执行任何CPU指令,用户进程只能通过内核提供的相应接口间接发起系统调用。因此虚拟地址空间也需要划分一部分用于映射内核使用的缓冲区,称为内核空间,剩余部分则被称为用户空间,顺便虚拟地址空间也达到了将内核空间和用户空间隔离的效果。这样每个进程都对应一组相同虚拟地址空间,其中内核空间全都映射到同一块物理内存,对所有进程一样,用户空间则被映射到不同的物理内存区域,各自独立。

当用户进程需要调用系统资源时,例如读取某个文件,就会通过内核提供的接口发起系统调用进入内核态,内核首先会从磁盘将文件读取到自身所在的内核空间,然后再从内核空间拷贝到进程所在的用户空间,最后切换回用户态,完成本次操作;当然对于写文件,也是类似操作,数据经历从用户空间到内核空间再到磁盘整个过程。由此我们可以看到,不管是读还是写文件,数据都经历了两次拷贝,在效率上是有一定损耗的,对于有高性能需求的场景中,传统的文件操作并不可靠,因此很多高性能模块在设计中都采用了 mmap 方案。

内存映射

mmap 是一种内存映射技术,可以将文件或对象映射到进程的内存地址空间,使得进程就像操作内存一样实现了“直接”对文件的高效读写。本质上来讲, mmap 实现的是内核缓冲区与用户进程的地址空间的映射,也就是说用户进程通过操作自己的逻辑虚拟地址就可以实现操作内核空间缓冲区,这样就不用再因为内核空间和用户空间相互隔离而需要将数据在内核缓冲区和用户进程所在内存之间来回拷贝。

mmap 属于操作系统提供的标准API,具体实现由操作系统完成,有一些比较值得注意的点:

  • mmap 在建立映射之初并没有物理内存分配,仅仅是从虚拟地址空间上划分出用于映射的区间并初始化相关数据结构,仅当首次读写文件触发缺页中断时才会开始分配内存并从磁盘读取文件数据,最后将分配物理内存的地址映射到用户进程的地址空间。
  • 由于分页机制,加载到内存的文件数据都是按页映射到虚拟地址空间,如果文件大小和页大小不是整数倍关系,则最后一页多余的部分会被填充补齐,则可能导致读写文件后最终文件实际内容存在乱码。
  • 由于在调用 mmap 方法时需要指定映射的大小,因此写文件的数据总大小是不能超过指定的大小的,即无法动态扩展。

我们重新回顾上面的读取文件的操作,当操作系统按页从磁盘加载文件到内核缓冲区后,由于该内存与用户进程地址空间存在直接映射,因此数据不用再经历内核空间到用户空间的拷贝操作,直接比标准的文件读取行为少了1次拷贝;对于写文件也是同理,用户进程对文件的修改行为直接反映到内核缓冲区上,无需再经历从用户空间到内核空间的拷贝,并且操作系统会自动对脏页面进行回写到磁盘文件,保证数据的可靠性。

Binder

作为Android系统进程间通信的实现,binder的核心实现也依赖 mmap 技术。前面说到虚拟内存地址空间将进程相互隔离,因此进程间通信必须通过内核空间中转,Android系统在启动时,内核会完成对binder驱动模块的加载,由binder驱动负责具体的IPC。当进行binder通信时,位于内核的binder驱动首先会开辟一块物理内存,然后在接收方进程的虚拟地址空间寻找一块空余区间与该物理地址建立映射,同时在内核的虚拟地址空间也划分一块区间与该物理地址建立映射,最后当发送方将数据通过划分的内核地址空间拷贝到内核中对应的物理内存时,由于该物理内存也映射到了接收方进程的虚拟地址空间,因此也相当于该数据被拷贝到了接收方进程用户空间,直接完成了进程间通信需求,整个过程只有1次从发送方进程用户空间到内核空间的拷贝操作。

最后

弄清楚 mmap 最主要是要理解虚拟地址空间的含义,这里说的空间只是一串数字而不是物理内存,我们可以把虚拟地址空间看作运营商的一系列电话号码,号码用户就可以看作是真实的物理内存,这些电话号码在任意时刻是否被卖出(使用),是A在使用还是B在使用都是动态变化的。mmap的行为就是找出未使用的电话号码分配给用户,不管这个用户在哪里(用户空间还是内核空间)都可以被呼叫。

参考