虚拟内存系统

OS X 和 iOS 中都有一个虚拟内存系统在持续运行。在32位的系统中,它为每个进程提供 4GB 的地址空间,64位的系统中提供 18EB 的地址空间。但是实际的内存容量远比这个地址空间要小得多,在 OS X 中,为了应用程序能访问全部的虚拟地址空间,系统会将部分数据备份在磁盘上。物理内存不足时,不用的数据会被写到磁盘上,即将要使用到的数据会从磁盘读入内存,这个过程称为页面交换。磁盘中有一个特殊分区,用于备份内存中暂时不用的数据,这个分区被称之为后备存储(backing store)。

页面交换只有在 OS X 中才有,iOS 并不支持页面交换,因此也没有所谓的后备存储。在 iOS 中,像代码页等只读数据,系统只是简单的将其加载到内存,这些只读数据不会被备份到磁盘,但有可能会从内存中清除(如程序处理挂起状态时),下次需要时内核会把已安装的应用程序镜像从磁盘重新加载到内存。iOS 永远不会将可写数据交换到磁盘上去,可用内存到达某一极限后,系统会请求当前正在运行的程序释放部分内存,为新的内存分配留出空间。仍然得不到足够的可用内存空间时,就会结束进程。

在32位的系统上地址空间限制在 4GB 大小, OS X 与其它操作系统的不同之处在于:整个 4GB 的地址空间都是可以在用户空间访问的,没有为内核预留空间。Windows 为内核预留了 2GB 的地址空间,Linux 只预留 1GB。严格地说,预留地址空间内的内存也可以被进程寻址访问,但是从用户态访问这些空间会产生通用保护错误,通常情况下会产生段错误,系统会杀掉进程。在64位模式下,由于巨大的地址空间,OS X 也遵循其它操作系统所采用的模型了,这样允许更快的用户态/内核态切换(共享 CR3 寄存器,CR3 寄存器是包含了页表指针的寄存器)。

初识虚拟内存

为了方便应用程序使用大内存,操作系统引入了虚拟内存,虚拟内存管理器会为每个进程创建一个逻辑地址空间(或称为虚拟地址空间),地址空间以页面(pages)为单位,对应的在物理内存中是块或页帧。内存管理单元(MMU)维护一个页表(page table)将逻辑地址空间中的页面映射到计算机RAM的硬件地址。当程序代码访问内存地址时,MMU使用页表将指定的逻辑地址转换成实际的硬件内存地址。

虚拟内存 vs. 物理内存
– 虚拟内存覆盖处理器的全地址空间;
– 物理内存只是虚拟内存中正在使用的部分;
– 不管是虚拟内存还是物理内存都被分隔成页面,且大小固定;
– CPU只识别虚拟内存地址;

页表是虚拟内存系统使用的一个数据结构,用于存储页面的虚拟地址与物理帧地址的映射关系,以及一些与页面有关的附加信息如:驻留标记位(present),“脏”标记位(或修改标记位)、进程ID等。每个进程都会有各自不同的页表,以保证每个进程虚拟地址不会映射到相同的物理地址上,以保证进程之间的独立性。

关于页表
– 页表包含与虚拟页面数量一样多的条目(entries);
– 保存在主存中;
– 直接将虚拟页面的序号做为页表的索引;

如果程序访问的逻辑内存地址所在的页面不在物理内存中,会产生一个缺页故障(page fault),这时虚拟内存系统立刻调用缺页故障处理器去处理缺页事件。缺页故障处理器会中断当前正在运行的代码,在物理内存中找到一个空闲页面,将磁盘中包含所需数据的页面加载到这个空闲页面,再更新页表,最后再把控制权交给程序代码。这个过程被称为页面调度(paging)。

如果在物理内存中没有可用的空闲页面,缺页故障处理器必须要把不活动的页面备份起来并且从RAM中删除,腾出更多的RAM给新的页面。如何释放物理页面依赖于平台,在 OS X 中,系统通常会把页面写入后备存储中。把数据从物理内存移动到后备存储的操作称为页面换出(paging out 或 swapping out),把数据从后备存储移动到物理内存的过程称为页面换入(paging in 或 swapping in)。在 iOS 中,没有后备存储,所以永远不会有页面被换出到磁盘,但是对于只读页面有可能在需要时会从磁盘上重新读入(是从安装的镜像而不是后备存储或交换分区读入)。

不管是 OS X 还是 iOS,页面大小总是 4KB,因此每次发生缺页故障时系统都会从磁盘上读取 4KB 大小的数据到内存。如果缺页故障频繁发生,系统就会消耗大量的时间读写页面,而不是执行代码了。

深入虚拟内存

每个进程的逻辑地址空间由映射内存区域(mapped regions of memory,后面简称区域)组成,映射的内存区域又包含一定数量的逻辑内存页,每个区域还包含一些控制属性如继承、写保护、是否固定(是否可以被换出)等。由于每个区域包含一定数量的页面,所以它们的大小是与页面边界对齐的,也就是说它们是以第一个页面的地址开始,以最后一个页面的结束地址结束。

内核给每一个逻辑地址空间的区域关联了一个 VM对象,VM对象主要包括以下字段:

  • 页面对象的链表 用来管理和跟踪该区域中哪些页面驻留在内存中;
  • 区域大小 该区域的大小,以字节为单位;
  • 分页器(pager) 负责将虚拟内存页面备份到特定类型的后备存储(可以是内存映射文件、设备或交换文件),或在需要时将这些页面读入内存。

除了分页器,VM对象还将区域(regions)关联到另一个VM对象上,内核使用这种自引用的技术实现写时拷贝(Copy-on-write)的区域。写时拷贝的区域可以让多个进程共享一个页面,只要这些进程都不会对这个页面执行写操作。一旦某个进程对共享页面执行了写操作,这个页面会被复制到这个进程单独的地址空间中,以后就可以对这个页面随时进行写入了。

有多种类型的分页器:
default pager 负责管理后备存储中的虚拟内存页面(很可能是多个页面),或者在需要时将这些页面读入内存;
vnode pager 负责内存映射文件的访问,当内存映射了文件,对这些内存的内容要从文件系统中读取。当内存映射的文件在内存中“脏”了(被修改过),那么这些脏的页面要写回文件系统。

固定内存(Wired Memory)

固定内存存储内核代码和数据,他们不能被置换到磁盘。应用程序、框架(Frameworks)和其它用户级别的软件不能分配固定内存,但是它们可以间接的影响固定内存的分配和释放。例如:某个应用程序创建了一个线程,会间接的为这个线程所需的内核资源分配固定内存。

内核中的页面链表

内核维护3个系统范围内的物理内存页面链表:

  • 活动链表 当前被映射进内存并且最近被访问的页面;
  • 不活动链表 在物理内存中但是最近未被访问的页面,这些页面持有有效数据,随时都有可能被移出内存;
  • 空闲链表 没有关联任何地址空间的物理内存页面,这些页面可能会被任何需要它们的进程使用;

当空闲链表中的页面数低于某个阈值(这个值依赖于物理内存的大小),分页器会尝试从不活动链表中移除部分页面,以使这三个链表中页面的数量达到一个平衡的状态。如果某个页面最近被访问过,它会重新被激活并插入到活动链表的末尾。在 OS X 中,如果某个不活动的物理内存页面数据最近未被写入后备存储,在它进入空闲链表前,数据内容必须要写入后备存储。在 iOS 中,修改过但是不活跃的页面仍然保留在内存中,但是内容会被拥有当前页面的应用程序清除。如果某个页面未被修改过并且也是固定的(wired),它会被转移到空闲列表,内容也会被清除。

内核在页面在未被访问时会将其从活动链表移动到不活动链表,在发生缺页软中断时会将页面从不活动链表移动到活动链表。当虚拟页面被交换出内存时,其关联的物理页面被插入空闲链表。

页面换出过程

在 OS X 中,空闲列表中的页面数低于某一阈值时,内核会以将不活动的页面交换出内存的方式收回物理页面。要完成此操作,内核遍历所有活动和不活动链表中的页面,然后执行如下操作:

  • 如果在活动链表中的页面最近未被访问,其会被移入不活动链表;
  • 如果在不活动链表中的页面最近未被访问,内核会找到页面的VM对象;
  • 如果VM对象之前未被初始化过,内核会调用初始化函数创建一个新的默认分页器并赋给VM对象;
  • VM对象的默认分页器尝试着将页面备份至后备存储;
  • 如果成功,内核释放被页面占用的物理内存,并将页面从不活动链表移入空闲链表;

页面换入过程

虚拟内存管理的最后阶段,是将页面移至物理内存,或从后备存储,或从包含页面的数据文件。当代码访问虚拟地址指向的数据未被映射进物理内存时,会引发内存访问故障。有两种类型的故障:

  • 软故障 代码访问虚拟内存地址所在的页面已经被映射到物理内存,但是没有映射到进程的地址空间;
  • 硬故障 代码访问虚拟内存地址所在的页面不在物理内存中,但被交换至后备存储,这被称为缺页故障;

上述两种故障中的任意一种发生时,内核会定位映射表项和该区域的VM对象,如果期望的页面在驻留页面链表中,内核会产生一个软故障,如果不在驻留页面链表中,会产生一个硬故障。

对软故障,内核映谢包含期望页面的物理内存到进程的虚拟地址空间,然后将该页面标记为活跃状态。如果有写操作,页面也会被标记成修改状态,以便在其被释放时,被写入后备存储。

对硬故障,VM对象的分页器会从后备存储(或磁盘上的文件,这依赖于分页器的类型)中查找到所需的页面,将其移至物理内存并放入活动链表。和软故障一样,有写操作发生时,页面会被标记为已修改状态。