预计阅读本页时间:-
11.5 内存管理
Windows Vista有一个极端复杂的虚拟内存系统。这一系统包括了大量Win32函数,这些函数通过内存管理器(NTOS执行层最大的组件)来实现。在下面章节中,我们将依次了解它的基本概念、Win32的API调用以及它的实现。
11.5.1 基本概念
在Windows Vista系统中,每个用户进程都有它自己的虚拟地址空间。对于x86机器,虚拟地址是32位的;因此,每个进程拥有4GB大小的虚拟地址空间。其中用户态进程的虚拟地址大小为2GB(在服务器系统中,用户态进程的虚拟地址大小可以配置成3GB)。另外的2GB(或1GB)空间为内核进程所用。对于运行在64位上的x64机器而言,地址可以是32位的也可以是64位的。32位地址是为了应用那些“需要通过WOW64来运行在64位系统上的32位进程”而保留的。由于内核拥有大量可用的地址空间,如果需要的话,32位进程可以使用全部4GB大小的地址空间。对于x86和x64机器,虚拟地址空间需要分页,并且页的大小一般都是固定在4KB——虽然在有些情况下每页的大小也可被分为4MB(通过只使用页目录而忽略掉页表)。
图11-30表示了三个x86进程的虚拟地址空间。每个进程的底部和顶端64KB的虚拟地址空间通常保留不用。这种做法是为了辅助发现程序错误而设置的。无效的指针通常标志为0或者-1,使用这样的指针会导致立即陷入中断,而不会读取垃圾信息、甚至写入错误的内存地址。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

从64KB开始为用户私有的代码和数据。这些空间可以扩充到几乎2个GB。而最顶端的2GB包含了操作系统部分,包括代码、数据、换页内存池和非换页内存池。除了每一进程的虚拟内存数据(像页表和工作集的列表),上面的2GB全部作为内核的虚拟内存、并在所有的用户进程之中共享。内核虚拟内存仅在内核态才可以访问。共享进程在内核部分的虚拟内存的原因是:当一个线程进行系统调用的时候,它陷入内核态之后不需要改变内存映射。所有要做的只是切换到线程的内核栈。由于进程在用户态下的页面仍然是可访问的,内核态下的代码在读取参数和访问缓冲时,就不用在地址空间之间来回切换、或者临时将页面进行两次映射。这里的权衡是通过用较小的进程私有地址空间,来换取更快的系统调用。
当运行在内核态的时候,Windows允许线程访问其余的地址空间。这样该线程就可以访问所有用户态的地址空间,以及对该进程来说通常不可访问的内核地址空间中的区域,例如页表的自映射区域。在线程切换到用户态之前,必须切换到它最初的地址空间。
1.虚拟地址分配
虚拟地址的每页处于三种状态之一:无效、保留或提交。无效页面(invalid page)是指一个页面没有被映射到一个内存区对象(section object),对它的访问会引发一个相应的页面失效。一旦代码或数据被映射到虚拟页面,就说一个页面处于提交(committed)状态。在提交的页上发生页面失效会导致如下情况:将一个包含了引起失效的虚拟地址的页面映射到这样的页面——由内存区对象所表示,或被保存于页面文件之中。这种情况通常发生在需要分配物理页面,以及对内存区对象所表示的文件进行I/O来从硬盘读取数据的时候。但是页面失效的发生也可能是页表正在更新而造成的,即物理页面仍在内存的高速缓存中,这种情况下不需要进行I/O。这些叫做软异常(soft fault),稍后我们会更详细地讨论它们。
虚拟页面还可以处于保留的(reserved)状态。保留的虚拟页是无效的,但是这些页面不能被内存管理器用于其他目的而分配。例如,当创建一个新线程时,用户态栈空间的许多页保留于进程的虚拟地址空间,仅有一个页面是提交的。当栈增长时,虚拟内存管理器会自动提交额外的页面,直到保留页面耗尽。保留页面的功效是:可以保证栈不会太长而覆盖其他进程的数据。保留所有的虚拟页意味着栈最终可以达到它的最大尽寸;而栈所需要的连续虚拟地址空间的页面,也不会有用于其他用途的风险。除了无效、保留、提交状态,页面还有其他的属性:可读、可写及可运行(在AMD64兼容的处理器下)。
2.页面文件
关于后备存储器的分配有一个有趣的权衡,已提交页面没有被映射于特定文件。这些页使用了页面文件(pagefile)。问题是该如何以及何时把虚拟页映射到页面文件的特定位置。一个简单的策略是:当一个页被提交时,为虚拟页分配一个硬盘上页面文件中的页。这会确保对于每一个有必要换出内存的已提交页,都有一个确定的位置写回去。
Windows使用一个适时(just-in-time)策略。直到需要被换出内存之前,在页面文件中的具体空间不会分配给已提交的页面。硬盘空间当然不需要分配给永远不换出的页面。如果总的虚拟内存比可用的物理内存少,则根本不需要页面文件。这对基于Windows的嵌入式系统是很方便的。这也是系统启动时的方式,因为页面文件是在第一个用户态进程smss.exe启动之后才初始化的。
在预分配策略下,用于私有数据(如栈、写时复制代码页)的全部虚拟内存受到页面文件大小的限制。通过适时分配的策略,总的虚拟内存大小是物理内存和页面文件大小的总和。既然相对物理内存来说硬盘足够大与便宜,提升性能的需求自然比空间的节省更重要。
有关请求调页,需要马上进行初始化从硬盘读取页的请求——因为在页入(page-in)操作完成之前,遇到页面失效的线程无法继续运行下去。对于失效页面的一个可能的优化是:在进行一次I/O操作时预调入一些额外的页面。然而,对于修改过的页写回磁盘和线程的执行一般并不是同步的。对于分配页面文件空间的适时策略便是利用这一点,在将修改过的页面写入页面文件时提升性能:修改过的页面被集中到一起,统一进行写入操作。由于只有当页面被写回时页面文件的空间才真正被分配,可以通过排列使页面文件中的页面较为接近甚至连续,来对大批写回页面时的寻找次数进行优化。
当存储在页面文件中的页被读取到内存中时,直到它们第一次被修改之前,这些页面一直保持它们在页面文件中的位置。如果一个页面从没被修改过,它将会进入到一个空闲物理页面的列表中去——这个表称作后备链表(standby list),这个表中的页面可以不用写回硬盘而再次被使用。如果它被修改,内存管理器将会释放页面文件中的页,并且内存将保留这个页的惟一副本。这是内存管理器通过把一个加载后的页标识为只读来实现的。线程第一次试图写一个页时,内存管理器检测到它所处的情况并释放页面文件中的页,再授权写操作给相应的页,之后让线程再次进行尝试。
Windows支持多达16个页面文件,通常覆盖到不同的磁盘来达到较高的I/O带宽。每一个页面文件都有初始的大小和随后依需要可以增长到的最大空间,但是在系统安装时就创建这些文件达到它的最大值是最好的。如果当文件系统非常满却需要增长页面文件时,页面文件的新空间可能会由多个碎片所组成,这会降低系统的性能。
操作系统通过为进程的私有页写入映射信息到页表入口,或与原页表入口相对应的共享页的内存区对象,来跟踪虚拟页与页面文件的映射关系。除了被页面文件保留的页面外,进程中的许多页面也被映射到文件系统中的普通文件。
程序文件中的可执行代码和只读数据(例如EXE或DLL)可以映射到任何进程正在使用的地址空间。因为这些页面无法被修改,它们从来不需要换出内存,然而在页表映射全部被标记为无效后,可以立即重用物理页面。当一个页面在今后再次需要时,内存管理器将从程序文件中将其读入。
有时候页面开始时为只读但最终被修改。例如,当调试进程时在代码中设定中断点,或将代码重定向为进程中不同的地址,或对于开始时为共享的数据页面进行修改。在这些情况下,像大多数现代操作系统一样,Windows支持写时复制(copy-on-write)类型的页面。这些页面开始时像普通的被映射页面一样,但如果试图修改任何部分页面,内存管理器将会建立一份私有的、可写的副本。然后它更新虚拟页面的页表,使之指向那个私有副本,并且使线程重新进行写操作——这一次将会成功。如果这个副本之后需要被换出内存,那么它将被写回到页面文件而不是原始文件中。
除了从EXE和DLL文件映射程序代码和数据,一般的文件都可以映射到内存中,使得程序不需要进行显式的读写操作就可以从文件引用数据。I/O操作仍然是必要的,但它们由内存管理器通过使用内存区对象隐式提供,来表示内存中的页面和磁盘中的文件块的映射。
内存区对象并不一定和文件相关。它们可以和匿名内存区域相关。通过映射匿名内存区对象到多个进程,内存可以在不分配磁盘文件的前提下共享。既然内存区可以在NT名字空间给予名字,进程可以通过用名字打开内存区对象、或者复制进程间的内存区对象句柄的方式来进行通信。
3.大物理内存寻址
多年前,当16位(或20位)的地址空间还作为标准的时候,机器已有兆字节的物理内存,人们努力想出各种技术使得程序可以使用更多的物理内存、而不是去适应有限的地址空间。这些技术通常基于存储器组转换(bank switching),使得一个程序可以突破16或者20位的限制,替换掉自己的一些内存块。在刚引入32位计算机时,大多数桌面计算机只有几个兆的物理内存。然而随着内存在集成电路上变得更加密集,可用内存开始迅速增长。这推动了服务器的发展,因为服务器上的应用程序往往需要更多的内存。英特尔的Xeon芯片支持物理地址扩展(PAE),物理内存寻址空间从32位变为36位,意味着一个单一的系统可以支持高达64GB的物理内存。这远远大于2G或者3G——单个进程可以在32位的用户模式寻址的虚拟地址空间,然而一些像SQL数据库这样的大型应用软件恰恰被设计为运行在一个单个进程的寻址空间中,因此存储器组转换已经过时了,取代它的是地址窗口扩展(Address Windowing Extensions,AWE)。这种机制允许程序(以正确的特权级运行)去请求物理内存的分配。进程可以保留所需的虚拟地址,并请求操作系统进行虚拟地址与物理地址间的映射。在所有的服务器应用64位寻址方式前,AWE一直充当权宜之计的角色。