预计阅读本页时间:-
11.5.3 存储管理的实现
运行在x86处理器上的Windows Vista操作系统为每个进程都单独提供了一个4GB大小的按需分页(demand-paged)的线性地址空间,不支持任何形式的分段。从理论上说,页面的大小可以是不超过64KB的2的任何次幂。但是在Pentium处理器上,页面正常情况下固定地设置成4KB大小。另外,操作系统可以使用4MB的页来改进处理器存储管理单元中的快表(Translation Lookaside Buffer,TLB)的效率。内核以及大型应用程序使用了4MB大小的页面以后,可以显著地提高性能。这是因为快表的命中率提高了,并且访问页表以寻找在快表中没有找到的表项的次数减少了。
调度器选择单个线程来运行而不太关心进程,存储管理器则不同,它完全是在处理进程而不太关心线程。毕竟,是进程而非线程拥有地址空间,而地址空间正是存储管理器所关心的。当虚拟地址空间中的一片区域被分配之后,就像图11-32中进程A被分配了4片区域那样,存储管理器会为它创建一个虚拟地址描述符(Virtual Address Descriptor,VAD)。VAD列出了被映射地址的范围,用来表示作为后备存储的文件以及文件被映射区域起始位置的节区以及权限。当访问第一个页面的时候,创建一个页目录并且把它的物理地址插入进程对象中。一个地址空间被一个VAD的列表所完全定义。VAD被组织成平衡树的形式,从而保证一个特定地址的描述符能够被快速地找到。这个方案支持稀疏的地址空间。被映射的区域之间未使用的地址空间不会使用任何内存中或磁盘上的资源,从这个意义上说,它们是“免费”的。

1.页面失效处理
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
当在Windows Vista上启动一个进程的时候,很多映射了程序的EXE和DLL映像文件的页面可能已经在内存中,这是因为它们可能被其他进程共享。映像中的可写页面被标记成写时复制(copy-on-write),使得它们能一直被共享,直到内容要被修改的那一刻。如果操作系统从一次过去的执行中认出了这个EXE,它可能已经通过使用微软称之为超级预读取(SuperFetch)的技术记录了页面引用的模式。超级预读取技术尝试预先读入很多需要的页面到内存中,尽管进程尚未在这些页面上发生页面失效。这一技术通过重叠从磁盘上读入页面和执行映像中的初始化代码,减小了启动应用程序所需的延时。同时,它改进了磁盘的吞吐量,因为使用了超级预读取技术以后,磁盘驱动器能够更轻易地组织对磁盘的读请求来减少所需的寻道时间。进程预约式页面调度(prepaging)技术也用到了系统启动、把后台应用程序移到前台以及休眠之后重启系统当中。
存储管理器支持预约式页面调度,但是它被实现成系统中一个单独的组件。被读入到内存的页面不是插入到进程的页表中,而是插入到后备列表中,从而使得在需要时可以不访问磁盘就将它们插入到进程中。
未被映射的页面稍微有些不同。它们没有被通过读取文件来初始化。相反,一个未被映射的页面第一次被访问的时候,存储管理器会提供一个新的物理页面,该页面的内容被事先清零(为了安全方面的原因)。在后续的页面失效处理过程中,未被映射的页面可能会被从内存中找到,否则的话,它们必须被从页面文件中重新读入内存。
存储管理器中的按需分页是通过页面失效来驱动的。在每次页面失效发生的时候,会发生一次到内核的陷入。内核将建立一个说明发生了什么事情的机器无关的描述符,并把该描述符传递给存储管理器相关的执行部件。存储管理器接下来会检查引发页面失效的内存访问的有效性。如果发生页面失效的页面位于一个已提交的区域内,存储管理器将在VAD列表中查找页面地址并找到(或创建)进程页表项。对于共享页面的情况,存储管理器使用与内存区对象关联的原始页表项来填写进程页表中的新页表项。
不同处理器体系结构下的页表项的格式可能会不同。对于x86和x64,一个被映射页面的页表项如图11-33所示。如果一个页表项被标记为有效,它的内容会被硬件读取并解释,从而虚拟地址能够转换成正确的物理地址。未被映射的页面也有对应的页表项,但是这些页表项被标记成无效,硬件将忽略这些页表项除该标记之外的部分。页表项的软件格式与硬件格式有所不同,软件格式由存储管理器决定。例如,对于一个未映射的页面,它必须在使用前分配和清零,这一点可以通过页表项来表明。
页表项中有两个重要的位是直接由硬件更新的,它们是访问位(access bit)和脏位(dirty bit)。这两个位跟踪了什么时候一个特定的页面映射用来访问该页面以及这个访问是否以写的方式修改了页面的内容。这确实很有助于提高系统性能。因为存储管理器可以使用访问位来实现LRU(Least-Recently Used,最近最少使用)类型的页面替换策略。LRU原理是,那些最长时间没有被使用过的页面有最小的可能性在不久的将来被再次使用。访问位使存储管理器知道一个页面被访问过了,脏位使存储管理器知道一个页面被修改了,或者更重要的是,一个页面没有被修改。如果一个页面自从从磁盘上读到内存后没有被修改过,存储管理器就没有必要在将该页面用到其他地方之前将页面内容写回磁盘了。
正如表11-33所示,x86体系结构通常使用32位大小的页表项,而x64体系结构使用64位大小的页表项。在域上面的唯一区别是x64的物理页号域是30位,而不是20位。然而,现今存在的任何x64处理器所支持的物理页面的数量都要远小于x64体系结构所能表示的数量。x86体系结构也支持一种特殊的物理地址扩展(Physical Address Extension,PAE)。PAE模式允许处理器访问超过4GB的物理内存,附加的物理页框位要求PAE模式下的页表项也是64位。

每个页面失效都可以归入以下五类中的一类:
1)所引用的页面没有提交。
2)尝试违反权限的页面访问。
3)修改一个共享的写时复制页面。
4)需要扩大栈。
5)所引用的页已经提交但是当前没有映射。
第一种和第二种情况是由于编程错误引起。如果一个程序试图使用一个没有一个有效映射的地址或试图进行一个称为访问违例(access violation)的无效操作(例如试图写一个只读的页面),通常的结果是,这个进程会被终止。访问破坏的原因通常是坏指针,包括访问从进程释放的和被解除映射的内存。
第三种情况与第二种情况有相同的症状(试图写一个只读的页面),但是处理方式是不一样的。因为页面已经标记为写时复制,存储管理器不会报告访问违例,相反它会为当前进程产生一个该页面的私有副本,然后返回到试图写该页面的线程。该线程将重试写操作,而这次的写操作将会成功完成而不会引发页面失效。
第四种情况在线程向栈中压入一个值,而这个值会被写到一个还没有被分配的页面的情况下发生。存储管理器程序能够识别这种特殊情况。只要为栈保留的虚拟页面还有空间,存储管理器就会提供一个新的物理页面,将该页面清零,最后把该页面映射到进程地址空间。线程在恢复执行的时候会重试上次引发页面失效的内存访问,而这次该访问会成功。
最后,第五种情况就是常见的页面失效。这种异常包含下述几种情况。如果该页是由文件映射的,内存管理器必须查找该页与内存区对象结合在一起的原型页表等类似的数据结构,从而保证在内存中不存在该页的副本。如果该页的副本已经在内存中,即在另一个进程的页面链表已经存在该页面的副本,或者在后备、已修改页链表中,则只需要共享该页即可。否则,内存管理器分配一个空闲的物理页面,并安排从磁盘复制文件页。
如果内存管理器能够从内存中找到需要的页而不是去磁盘查找从而响应页面失效,则称为软异常(soft fault)。如果需要从磁盘进行复制,则称为硬异常(hard fault)。软异常同硬异常相比开销更小,对于应用程序性能的影响很小。软异常出现在下面场景中:一个共享的页已经映射到另一个进程;请求一个新的全零页,或所需页面已经从进程的工作集移除,但是还没有重用。
当一个物理页面不再映射到任何进程的页表,将进入以下三种状态之一:空闲、修改或后备。内存管理器会立刻释放类似那些已结束进程的栈页面这样不再会使用的页面。根据判断映射页面的页表项中的上次从磁盘读出后的脏位是否设置,页面可能会再次发生异常,从而进入已修改链表或者后备链表(standby list)。已修改链表中的页面最终会写回磁盘,然后移到后备链表中。
内存管理器可以根据需要从空闲链表或者后备链表中分配页面。它在分配页面并从磁盘复制之前,总是在已修改链表和后备链表中检查该页面是否已经在内存中。Windows Vista中的预约式调页机制通过读入那些未来可能会用到的页面并把它们插入后备链表的方式将硬异常转化为软异常。内存管理器通过读入成组的连续页面而不是仅仅一个页面来进行一定数量的普通预约式调页。多余调入的页面立刻插入后备链表。而由于内存管理器的开销主要是进行I/O操作引起的,因而预约式调页并不会带来很大的浪费。与读入一簇页面相比,仅读入一个页面的额外开销是可以忽略的。
图11-33中的页表项指的是物理页号,而不是虚拟页号。为了更新页表(以及页目录)项,内核需要使用虚拟地址。Windows使用如图11-34所示的页目录表项中的自映射(self-map)表项将当前进程的页表和页目录映射到内核虚拟地址空间。通过映射页目录项到页目录(自映射),就具有了能用来指向页目录项(图11-34a)和页表项(图11-34b)的虚拟地址。每个进程的自映射占用4MB内核地址空间(x86上)。幸运的是,该4MB地址空间是同样一块地址空间。

2.页面置换算法
当空闲物理页面数量降得较低时,内存管理器开始从内核态的系统进程以及用户态进程移走页面。目标就是使得最重要的虚拟页面在内存中,而其他的在磁盘上。决定什么是重要的需要技巧。Windows通过大量使用工作集来解决这一问题。工作集处在内存中,不需要通过页面失效即可使用的映射入内存的页面。当然,工作集的大小和构成随着从属于进程的线程运行来回变动。
每个进程的工作集由两个参数描述:最小值和最大值。这两个参数并不是硬性边界,因而一个进程在内存中可能具有比它的工作集最小值还小的页面数量(在特定的环境下),或者比它的工作集最大值还大得多的页面数量。每个进程初始具有同样的最大值和最小值的工作集,但这些边界随着时间的推移是可以改变的,或是由包含在作业中的进程的作业对象决定。根据系统中的全部物理内存大小,这个默认的初始最小值的范围是20~50个页面,而最大值的范围是45~345个页面。系统管理员可以改变这些默认值。尽管一般的家庭用户很少去设置,但是服务器端程序可能需要设置。
只有当系统中的可用物理内存降得很低的时候工作集才会起作用。其他情况下允许进程任意使用它们选择的通常远远超出工作集最大值的内存。但是当系统面临内存压力的时候,内存管理器开始将超出工作集上限最大的进程使用的内存压回到它们的工作集范围内。工作集管理器具有三级基于定时器的周期活动。新的活动会加入到相应的级别。
1)大量的可用内存:扫描页面,复位页面的访问位,并使用访问位的值来表示每个页面的新旧程度。在每个工作集内保留使用一个估算数量的未使用页面。
2)内存开始紧缺:对每个具有一定比例未用页面的进程,停止为工作集增加页面,同时在需要增加一个新的页面的时候换出最旧的页面。换出的页面进入后备或者已修改链表。
3)内存紧缺:消减(也即减小)工作集,通过移除最旧的页面从而降低工作集的最大值。
平衡集管理器(balance set manager)线程调用工作集管理器,使得其每秒都在运行。工作集管理器抑制一定数量的工作从而不会使得系统过载。它同时也监控要写回磁盘的已修改链表上的页面,通过唤醒ModifiedPageWriter线程使得页面数量不会增长得过快。
3.物理内存管理
上面提到了物理页面的三种不同链表,空闲链表、后备链表和已修改链表。除此以外还有第四种链表,即全部被填零的空闲页面。系统会频繁地请求全零的页面。当为进程提供新的页面,或者读取一个文件的最后部分不足一个页面时,需要全零页面。将一个页面写为全零是需要时间的,因此在后台使用低优先级的线程创建全零页是一个较好的方式。另外还有第五种链表存放有硬件错误的页面(即通过硬件错误检测)。
系统中的所有页面要么由一个有效的页表项索引,要么属于以上五种链表中的一种,它们的全体称为页框号数据库(PFN数据库)。图11-35表明PFN数据库的结构。该表格由物理页框号索引。表项都是固定长度的,但是不同类型的表项使用不同的格式(例如共享页面相对于私有页面)。有效的表项维护页面的状态以及指向该页面数量的计数。工作集中的页面指出哪个表项索引它们。还有一个指向该页的进程页表的指针(非共享页),或者指向原型页表的指针(共享页)。

此外还有一个指向链表中下一个页面的指针(如果有的话),以及其他的若干诸如正在进行读和写的域以及标志位等。这些链表链接在一起,并且通过下标指向下一个单元,不使用指针,从而达到节省存储空间的目的。另外用物理页面的表项汇总在若干指向物理页面的页表项中找到的脏位(即由于共享页面)。表项还有一些别的信息用来表示内存页面的不同,以便访问那些内存速度更快的大型服务器系统上(即NUMA-非均衡存储器访问的机器)。
工作集管理器和其他的系统线程控制页面在工作集和不同的链表间移动。下面对这些转变进行研究。当工作集管理器将一个页面从某个工作集中去掉,则该页面按照自身是否修改的状态进入后备或已修改链表的底部。这一转变在图11-36的(1)中进行了说明。

这两个链表中的页面仍然是有效的页面,当页面失效发生的时候需要它们中的一个页,则将该页移回工作集而不需要进行磁盘I/O操作(2)。当一个进程退出,该进程的非共享页面不能通过异常机制回到以前的工作集,因此该进程页表中的有效页面以及挂起和已修改链表中的页面都移入空闲链表(3)。任何该进程的页面文件也得到释放。
其他的系统调用会引起别的转变。平衡集管理器线程每4秒运行一次来查找那些所有的线程都进入空闲状态超过一定秒数的进程。如果发现这样的进程,就从物理内存去掉它们的内核栈,这样的进程的页面也如(1)一样移动到后备链表或已修改链表。
两个系统线程——映射页面写入器(mapped page writer)和已修改页面写入器(modified page writer),周期性地被唤醒来检查是否系统中有足够的干净页面。如果没有,这两个线程从已修改链表的顶部取出页面,写回到磁盘,然后将这些页面插入后备链表(4)。前者处理对于映射文件的写,而后者处理页面文件的写。这些写的结果就是将已修改(脏)页面移到后备(干净)链表中。
之所以使用两个线程是因为映射文件可能会因为写的结果增长,而增长的结果就需要对磁盘上的数据结构具有相应的权限来分配空闲磁盘块。当一个页面被写入时如果没有足够的内存,就会导致死锁。另一个线程则是解决向页面文件写入页时的问题。
下面说明图11-36中另一个转换。如果进程解除页映射,该页不再和进程相关从而进入空闲链表(5),当该页是共享的时候例外。当页面失效会请求一个页框给将要读入的页,此时该页框会尽可能从空闲链表中取下(6)。由于该页会被全部重写,因此即使有机密的信息也没有关系。
栈的增长则是另一种情况。这种情况下,需要一个空的页框,同时安全规则要求该页全零。由于这个原因,另一个称为零页面线程(ZeroPage thread)的低优先级内核线程(参见图11-28)将空闲链表中的页面写全零并将页面放入全零页链表(7)。全零页面很可能比空闲页面更加有用,因此只要当CPU空闲且有空闲页面,零页面线程就会将这些页面全部写零,而在CPU空闲的时候进行这一操作也是不增加开销的。
所有这些链表的存在导致了一些微妙的策略抉择。例如,假设要从磁盘载入一个页面,但是空闲链表是空的,那么,要么从后备链表中取出一个干净页(虽然这样做稍后有可能导致缺页),要么从全零页面链表中取出一个空页(忽略把该页清零的代价),系统必须在上述两种策略之间做出选择。哪一个更好呢?
内存管理器必须决定系统线程把页面从已修改链表移动到后备链表的积极程度。有干净的页面后备总比有脏页后备好得多(因为如有需要,干净的页可以立即重用),但是一个积极的净化策略意味着更多的磁盘I/O,同时一个刚刚净化的页面可能由于缺页中断重新回到工作集中,然后又成为脏页。通常来讲,Windows通过算法、启发、猜测、历史、经验以及管理员可控参数的配置来做权衡。
总而言之,内存管理需要一个拥有多种数据结构、算法和启发性的十分复杂、重要的构件。它尽可能地自我调整,但是仍然留有很多选项使系统管理员可以通过配置这些选项来影响系统性能。大部分的选项和计数器可以通过工具浏览,相关的各种工具包在前面都有提到。也许在这里最值得记住的就是,在真实的系统里,内存管理不仅仅是一个简单的时钟或老化的页面算法。