关于系统虚拟内存

对于OS X以及IOS而言,使用高效的内存管理技术是书写高水平代码的重要表现之一。最小化你的内存使用不仅可以降低你的程序的内存占用水平,同时也可以减少因此带来的CPU的相关耗费。为了能够更好的优化你的代码,你必须明白系统底层是如何管理内存的。

OS X以及IOS都包含了完整集成的虚拟内存系统,用户无法主动关闭它,它一直是开启状态。OS XIOS针对32位进程均提供了高达4GB的内存地址访问空间。此外OS X还针对64位进程提供了高达18EB的内存地址访问空间。即使计算机的内存大于4GB,系统也鲜会给单个进程分配大于4GB的内存空间。

为了能够给足单个进程所能分配的最大内存空间(32位是4GB,64位是18EB),OS X使用硬盘去存储那些目前不再使用的数据。随着内存逐渐占用变满,部分不再使用的内存数据将会被写入硬盘,从而为需要写入内存的新的数据腾出空间。那些存储暂时不再使用数据的硬盘空间被称为备份存储空间,因为它为内存提供了备份支持。

尽管OS X提供了备份存储支持,但是IOS却并没有提供。在Iphone的app应用中,已经存储在硬盘上的只读数据(例如存储代码的页)只会当系统需要时直接从硬盘中加载入内存,当系统不需要时直接从内存中释放。可写数据是绝对不会被操作系统从内存中移除的。与之对应的是,如果IOS可用内存低于某一阈值,系统会要求正在运行的app应用程序为即将加入内存的新数据释放内存空间。如果该app没有及时释放足够的内存,那么系统将会直接终结掉该app。

注释:不同于基于UNIX的大多数系统,OS X不会使用预先分配的硬盘空间做备份存储而是使用系统根目录下的所有空间作为可能的备份存储空间。

下面几节将介绍相关术语并对在OS X以及IOS中使用的虚拟内存系统做一个简要介绍。如果想了解更多有关虚拟内存的工作原理,请阅读《Kernel Programming Guide》。

关于虚拟内存

虚拟内存使得操作系统摆脱物理内存(RAM)大小限制所带来的束缚。虚拟内存管理器(VVM)为每个进程创建了一种称作逻辑地址的空间(或者被称为“虚拟”地址空间),并将这些地址空间分割成了大小一致的块,这样的块称之为“”。进程还有它的内存管理单元(MMU)维护了一个页表,该页表将程序的逻辑内存地址空间和实际的物理内存地址空间做了映射。当程序代码访问内存地址(逻辑地址)时,内存管理单元会负责将逻辑地址转换为实际的物理地址。这种转换是自动进行的并对正在运行的app是透明的。

作为一个程序而言,它的逻辑地址空间中的地址总是可用的(可访问)。然而,如果一个应用app访问了一个没有在物理内存中有映射关系的逻辑内存页,那么页错误就产生了。当这种错误产生时,虚拟内存系统会立刻唤起一个特殊的页错误处理器来响应这个错误。这个页错误处理器会首先暂停目前正在运行的代码,然后在物理内存中开辟一页新的空闲内存空间,将需要从硬盘中载入内存的数据写入该页中,然后更新页表,最后将控制权重新移交给程序代码,从而使的程序代码能够正常访问内存地址(逻辑内存地址)。这个过程我们称之为分页(Paging)

当然,如果物理内存空间没有足够的空闲页的话,页错误处理器必须首先在物理内存中释放一个已经被占用的页来换得新的空闲页。系统如何释放页取决于系统平台。对于OS X而言,虚拟内存系统通常会将页写入硬盘中的备份存储空间。这个备份存储空间备份了内存中的页的数据信息。将数据从物理内存移动至备份存储空间的过程称之为页出(Page Out)。将数据从备份存储空间载入内存的过程称之为页入(Page in)。在IOS中,由于没有备份存储机制,所以页从来不会从内存交换至硬盘中去,但是当需要时只读页还是会被从硬盘载入至内存中去的。

无论OS X或者IOS,页的大小均为4K。因此,每发生一次页错误,系统都会从硬盘中读取4K数据。当系统花费了大量时间处理页错误以及页的读写操作而不是用来处理程序代码时,就会产生磁盘震颤( disk thrashing )现象。

任何形式的分页(Paging),以及特别是磁盘震颤现象都会对系统性能产生消极影响,因为它们会迫使系统花费大量时间进行磁盘读写。从备份存储空间读取数据所耗费时间要远高于直接从内存中读取数据。此外,如果系统在从硬盘中读取某一页数据之前必须要先写入一页数据,那么对于系统性能造成的影响将会更糟。

虚拟内存系统细节介绍

一个进程的逻辑地址空间包含着若干物理内存映射区域。每个物理内存映射区域都又包含若干已知数量的虚拟内存页。每个物理内存映射区域都通过一些特殊的属性标签将这些页分为不同的类型,例如是否是写保护,或者是否是关联内存页(如果是的话,那么它将不会执行页出操作)。又因为每个物理内存区域包含有已知数量的页,所以每个区域都是页对齐( page-aligned )的,这意味着这个物理内存映射区域的首地址即为页的首地址,物理内存区域的末地址即为页的末地址。

内核将每个逻辑地址空间区域与一个VM(虚拟内存)对象关联到一起。内核使用VM对象去追踪管理关联内存区域中留存在内存和暂时留存在内存中的页。一个内存区域可以映射备份存储空间中的某一部分,也可以映射文件系统中的某些文件。每一个VM对象都会将其对应的内存区域与默认分页器(default pager)或者vnode分页器(vnode pager)建立关联。默认分页器是指管理着被页出至备份存储区域的虚拟内存页并且当系统需要时重新将这些页写回内存的管理器。vnode分页器使用分页机制提供了一个直接面向文件的窗口。这个机制使得当文件驻存在内存中时,你可以读写器中的某些部分。

除了将内存区域映射至默认分页器或者vnode分页器外,VM对象也可能将区域映射至另一个VM对象。内核使用这种自引用技术来实现copy-on-write内存区域。该内存区域允许多进程(或者一个进程中的多个blok)在不进行更改写入操作的前提下共享一个页面,当某一个进程试图对该页进行写入操作时,该进程就会在其逻辑地址空间对该页进行拷贝,创建一个拷贝页来进行写入操作。自此,执行写入操作的进行就会维护该拷贝页使得它可以在任何时间进行写入操作。copy-on-write内存区域使得系统内存共享了大量数据,且又可以保证不同进程对某内存进行直接又安全的操作。通常系统框架载入的内存区域往往是上述所说的内存区域类型。

每一个VM对象都包含有如下字段,如下所示:

  • Resident pages : 该区域内常驻物理内存的页的链表
  • Size : 该区域大小
  • Shadow : 用于copy-on-write优化
  • Copy : 用于copy-on-write优化
  • Attributes : 该区域执行不同操作的标记

如果VM对象涉及到copy-on-write操作,那么shadow和copy字段会指向另一个VM对象。否者这两个字段都会指向NULL。

关联内存

关联内存(也叫驻留内存)通常存储内核代码和相关数据结构,这部分内存是绝对不会被页出至硬盘中去的。应用,架构,或者其他用户层级的软件是不能够申请关联内存的。然而,它们却可以影响某一时刻有多少关联内存存在。例如,一个会隐式创建数条线程和端口的应用就会影响到关联内存,因为创建线程和端口都会需要系统内核资源,这其中就包含着关联内存。 如下列出了一个应用程序产生导致相应的关联内存耗费关系:

  • 进程 : 16k
  • 线程 : blocked in a continuation 5k; blocked 21K
  • Mach 端口 : 116B
  • Mapping : 32B
  • Library :2K + n*200B (n代表使用该Library的task数量)
  • Memory region : 160B

注意:这些数据可能会随着操作系统的升级而发生改变,列在这里是给你一个直观的耗费关系。

正如你所看到的,每一个线程,每一个进程,每一个library都对系统造成了内存占用。除了你的应用不同程度的使用者关联内存,系统内核自己的一些实体也需要关联内存,如:

  • VM对象
  • 虚拟内存缓存
  • I/O缓存
  • 驱动

关联内存同样也关联着物理内存页面以及对应的页表。上述这些实体也会随着可用物理内存的增加而增加。当你为系统增加内存后,及时没有做任何其它改变,关联内存的大小也会增加。当电脑第一次启动加载至Mac的Finder下,没有任何其它应用此时在运行。关联内存也会大概占用64兆系统大小中的14兆,128兆系统大小中的17兆。

当关联内存页无效时,它们不会立刻被移动至空闲链表中去,相对应的,它们会当因空闲页面数值低于阈值触发页出操作时,被当作“垃圾回收走”。

内核中的页链表

内核维护并且时常访问三条系统级别的物理内存链表:

  • 活跃(内存)链表,它主要包含那些经常被映射到内存中去,经常被访问的页。
  • 非活跃(内存)链表,它主要包含那些经常驻留在内存里,但是最近已经没有被访问的页。这些页中都包含着有效的数据,但是它们随时可能会从内存中被删除掉(OS X会转移到硬盘,IOS会要求用户处理,否则就直接删除)
  • 空闲(内存)链表,包含那些没有被虚拟内存地址所关联的空闲的物理内存页。这些页在系统需要它们的时候可以立即被使用。

空闲链表的可用页数小于某个阈值时(主要由物理内存的大小决定),分页器就会试图去平衡上述3个链表所包含的页数(实际意思是就开始要增加空闲链表的长度,提升可用的空闲页的数量),采取方法的途径就是从非活跃链表中取出非活跃页填补至空闲页。如果非活跃链表中某一页最近又被访问了,那么它将重新激活为活跃页,并被放入活跃链表的尾端。在OS X平台下,如果一个包含数据的非活跃页最近没有被写回至存储备份区域,那么它必须被执行页出操作,将数据写至硬盘的存储备份区域,然后它将会被放置到空闲链表中去(在IOS中,已经修改过的非活跃页必须驻留在内存中,由应用程序自己清理掉它)。如果一个非活跃页没有被修改且又不是关联内存类型的页的话,那么对应它的虚拟内存映射将会被销毁,然后被添加至空闲链表中去。一旦空闲链表中空闲页数量超过了最低阈值,此时分页器停止工作。

内核同样会将没有被访问的页从活跃链表移动至非活跃链表中去;当发生软错误(soft fault)时,内核会将页从非活跃链表移动至活跃链表。当内存中的逻辑地址页(虚拟页)被从内存中交换出至硬盘中时,其对应的物理页将被放置在空闲链表中去,当然,当进程显式地释放空闲内存时,内核同样会进行上述操作。

页出过程

OS X平台下,当空闲链表中的空闲页数量低于某个经过计算的阈值后,内核会将非活跃链表中的页交换出内存,从而为空闲链表页提供可用的物理内存页。为了实现上述操作,内核会迭代所有活跃链表非活跃链表中的页,并执行如下操作:

  1. 如果某一个活跃链表中的页最近没有被访问,那么它将被移动至非活跃链表中。
  2. 如果某一个非活跃链表中的页最近没有被访问,那么内核将会找到该页对应的VM对象。
  3. 如果该VM对象在此之前从来没有被分页(paged),内核会创建并指派给该VM对象一个默认分页器
  4. 这个VM对象的默认分页器会尝试将该VM对象下的第2步操作所述的页进行页出操作至硬盘中的备份存储区域。
  5. 如果分页器执行操作成功,内核会释放该页所对应的物理内存,并将该页从非活跃链表移动至空闲链表中去。

提示:在IOS中,内核不会执行将页换出至备份存储区域的操作,当空闲链表中的空闲页数量低于某个经过计算的阈值后,内核会刷新那些身在非活跃链表且未经修改的页,也可能会要求应用程序本身直接执行释放操作。更多的信息请看IOS的低内存警告应对措施

页入过程

虚拟内存管理的最后一个步骤就是将页从硬盘备份存储区域或者包含页数据的文档中重新移动至物理内存中来。一般内存访问错误会激活页入操作进程。当代码试图访问一个没有与物理内存建立映射关系的虚拟内存地址时,内存访问错误就会发生。

有如下两种错误类型:

  • 软错误(soft fault),当该页的相关地址驻留在物理内存中,但是目前却没有映射至该进程的地址空间中,这时候访问该页虚拟内存地址发生的错误叫做软错误。
  • 硬错误(hard fault),当该页的相关地址没有在物理内存中,而是被页出至硬盘存储备份区域。这时候访问该页虚拟内存地址发生的错误叫做硬错误。

无论上述哪种错误发生,内核会锁定错误发生的页的映射入口和该页所在区域对应的VM对象。然后内核会遍历该VM对象的驻留物理内存的页链表。如果目标页在该驻留内存页链表上,内核即产生软错误(soft fault)。反之,如果目标页不再驻留内存页链表上,那么内核产生硬错误(hard fault)

对于软错误(soft fault),内核会将包含该页的物理内存与该进程的虚拟地址空间建立映射,然后内核将该页标记为活跃页。如果发生软错误的页还涉及到了写操作,那么该页也会被标记为 被修改页 ,这样,如果将来它被释放的时候会先写入硬盘中的备份存储空间中去。

对于硬错误(hard fault),VM对象分页器(Pager)会依据该页的类型在硬盘中的备份存储区域找到该页的物理存储地址。然后对VM对象的映射信息作出适当修改后,就会将该页重新移回至物理内存,将该页放入活跃(内存)链表。与软错误一样,如果发生错误的页还涉及到写操作,则该页会被标记为 被修改页