关于内存映射的备忘录

DYX

虚拟内存页的分类

在大多数UNIX系统上,一个虚拟内存页可以关联到一个普通文件或者一个匿名文件。一个匿名文件并不是真正存在的磁盘数据,而是由内核维护的一个对象,里面包含的全部是二进制零。这里把关联到普通文件的页叫做文件页,把关联到匿名文件的页叫做匿名页。

分配了虚拟页之后,在我们未访问它之前,其并不会占用物理内存。对于关联到普通文件的页,当第一次访问发生的时候,内核通过缺页异常处理程序从磁盘文件将对应数据载入物理内存。对于关联到匿名文件的页,则直接分配一块物理内存并填充二进制零,不需要发生磁盘I/O。

在另一个维度上,虚拟内存页可以被分为共享的和私有的。一个进程对一个共享页的修改,在另一个进程中是可见的。对私有页的修改则是不可见的。如果关联到普通文件,那么系统会在适当的时候把对共享页的修改写回到文件,而对私有页则不会。

匿名页

当一个匿名页(无论共享还是私有)需要被牺牲时,由于没有普通文件和它对应,所以它会在交换设备中分配空间并写入对应的数据。唯一的例外发生在从未被修改过的页身上,由于从未被修改过的匿名页包含的全部都是二进制零,所以没有必要把它的数据写入磁盘,下次载入物理内存时可以直接填充二进制零。

私有匿名页

子进程在fork之后,会继承父进程的地址空间,对于其中的私有匿名页,将会采用写时拷贝策略。也就是一开始的时候,映射到同一个物理页或交换设备中的同一个地址,并不会占用额外的物理空间。当父或子进程要写这个页时,才会生成一份拷贝,并把进程中的对应地址映射到新的拷贝上。

在Linux中,可执行文件和共享库的.bss段,堆段,栈段,通常会被加载器以私有匿名页的形式映射到虚拟地址空间。

共享匿名页

对于共享匿名页,子进程将和父进程映射到同样的对象,互相的写操作都是可见的,因此可以用于父子进程的通信。

Linux内核为进程分配的内存段中,没有使用这种形式的。

文件页

私有文件页

和私有匿名页一样,私有文件页也采用写时拷贝策略。和匿名页不一样的地方在于,由于进程可以指定要关联的文件,因此其不仅可以在父子进程中映射到相同对象,还可以在任何两个进程中映射到同一个对象。这使得我们可以让任意两个进程拥有两块含有相同数据的虚拟地址空间却不占用额外的物理内存,直到其中一个进程发出写操作,才会生成自己的拷贝。

因为不会回写关联的文件,所以在必要的时候私有文件页也会在物理内存和交换设备间换入换出。也就是当一个私有文件页需要被牺牲时,它会在交换设备中分配空间并写入对应的数据。

在Linux中,可执行文件和共享库的.init段,.text段,.rodata段,.data段通常以私有文件页的形式被映射到虚拟地址空间。但除.data段之外都会设置只读标志位,如果进程尝试写入的话会引发段错误。

共享文件页

由于可以指定要关联的文件,跨进程的修改又是可见的,所以共享文件页可以用来在任意两个进程之间同步数据。另一方面,因为有关联的普通文件,所以一个共享文件页不会被换出到交换设备,而是采用写回策略同步物理内存到关联的磁盘文件。所谓写回策略,是指内核会根据此页是否被标记为dirty(即上次载入物理内存后是否受到修改)来决定是否将此页数据写入磁盘对应的位置。内核会在适当的时候执行写回,例如当一个共享文件页需要被牺牲以节约物理内存的时候。用户代码可以调用msync函数来手动执行写回。

从上面的讨论可以看出,以共享方式映射到普通文件的虚拟地址空间,其行为和这个文件的page cache一致。事实上,Linux中的共享文件页指向的物理内存就是文件的page cache。

Linux内核为进程分配的用户空间内存段中,没有使用这种形式的。

参考文档