前言

之前研究 mmap 发现有一个地方不是非常理解,为什么使用 mmap 完成了文件到进程地址空间的映射还要调用 msync 来进行同步?

我经过查找资料后找到了答案,由于讲到的人比较少,所以便有了此文,希望对同样有困惑的人有帮助。

虚拟内存

早期的操作系统中的进程是与其它进程共享主存资源的,它采用物理寻址。这样内存容易被破坏,如果某个进程不小心写了另一个进程使用的内存,它就有可能以某种完全和程序逻辑无关的令人迷惑的方式失败。还有一种情况就是如果一个进程需要太多的内存,另外一些进程没有足够的内存则无法运行。

为了更加有效的管理内存并减少出错,现代操作系统提供了一种对主存的抽象概念,也就是虚拟内存。虚拟内存提供了如下三个能力

  1. 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据。

    在整个运行过程中,程序引用的内存可能会超过物理内存的总大小,但是局部性原则保证了在任意时刻,程序将趋向于在一个较小的活动区域上工作。操作系统采用分页的方式来管理内存,所以内存都是按页加载。

  2. 它为每个进程提供了一致的地址空间,从而简化了内存管理。

    操作系统内部通过 MMU(内存管理单元)将虚拟内存与物理内存建立映射关系,所以对进程来说它们拥有相同内存大小而且是相互独立的。以32位系统为例,进程的寻址空间是4G(232)。

  3. 它保护了每个进程的地址空间不被其它进程破坏。

    由于使用MMU所以从进程的角度来看,它是一组非负整数的连续地址的有序集合。这块地址映射到RAM哪一块是不知道的,所以不会访问到其它进程的内存。也就达到了进程隔离的目的。

用户空间/内核空间

在虚拟内存中我们提到了每个进程都有独立的虚拟地址,它被分为用户空间和内核空间。内核空间在高地址,大小为1G,用户空间则是剩余的3G,在低地址。

划分用户空间与内核空间是出于安全性的考虑,因为有些操作比较危险,交给应用程序操作可能导致整个系统崩溃,所以这部分操作需要由内核处理,使得进程异常退出了也不会影响内核和其它进程。

CPU将指令分为特权指令和非特权指令,对于危险的指令只允许操作系统使用,普通应用程序不能使用。英特尔的CPU将特权等级分为4个级别:Ring0~Ring3。Linux只使用了Ring0和Ring3两个级别,Ring0就是运行在内核态,Ring3则是用户态。用户态需要通过系统调委托内核态做一些特权操作。

比如我们熟悉的读取文件中的数据,需要通过 read 发起系统调用陷入内核态,内核会从磁盘将文件读取到内核的缓冲中,然后将数据拷贝到用户空间,最后从内核态切换到用户态。这样的操作有点麻烦,数据需要拷贝到内核然后又要拷贝到用户空间,非常麻烦,而且用户态和内核态之间的上下文切换,对性能是有一定的影响的。

所以在一些对性能有较高要求的方案中都使用了 mmap 来减少拷贝的次数。

内存映射

mmap 是内存映射的技术,它通过将文件映射到进程的地址空间,我们就可以像操作内存一样操作文件了,而不再需要调用 read/write 来进行对文件的读写。

实际上这里所说的将文件映射到进程的地址空间,指的是内核中的文件的页缓存与进程中的地址空间的映射,不是文件本身。这也就回答了文章开头的问题。

当初我以为是直接操作文件,一直不明白如果是直接操作文件干嘛还要调用 munmap 或者 msync 进行同步。

有了上面的内容做铺垫,就可以理解为什么在程序异常退出的时候还能保证数据不会丢失。因为我们在进程的地址空间操作就是操作页缓存,而页缓存是属于内核的。所以在应用程序崩溃的时候,内核还是正常的不会收到影响,所以这部分数据没有丢失。

内核在得知应用程序退出后会被动的把数据回写到文件中。

内核回写数据到文件中有这么几个时机

  1. 调用 msync 函数主动进行数据同步
  2. 调用 munmap 函数对文件进行解除映射关系时
  3. 进程退出时
  4. 系统关机时

mmap 除了不会丢数据还有一个特点就是性能要优于文件的读写,这是由于 mmap 完成了内核页缓存与进程中的地址空间的映射,所以不需要把用户空间的数据再一次拷贝到内核空间,并且这样也减少了系统调用导致的上下文切换。

由于 mmap 映射的是内核页,所以使用 mmap 的时候要注意,要按照页大小来进行映射。通常页大小是 4k。

使用 mmap 完成映射的时候并不会马上读取数据,而是需要等到程序对这个内存进行访问时发现这一段地址不在物理内存上(因为只是建立了地址映射),所以引发缺页中断,这时候才会把数据读到主存中。

参考