Windows内核编程
上QQ阅读APP看书,第一时间看更新

1.2 虚拟内存

每个进程都有自己的虚拟、私有、线性的地址空间。这个地址空间起始是空的(或者接近于空,因为可执行映像和NTDLL.dll首先被映射到内存中,继而是更多的子系统DLL)。一旦主(第一个)线程开始执行,很可能就会开始分配内存,同时会有更多的DLL装入地址空间中,等等。这个地址空间是私有的,意味着别的进程无法直接访问。地址空间从0开始(虽然从技术上来说,第一个64KB的地址是不能以任何形式分配或使用的),一直增长到最大值,最大值依赖于进程的“位数”(32或64位)和操作系统的“位数”。规则如下:

  • 32位Windows系统上的32位进程,进程地址空间的默认大小是2GB。
  • 32位Windows系统上的32位进程,且使用了增加用户地址空间的设置(PE文件头里的LARGEADDRESSAWARE标志),最大地址空间可以达到3GB(取决于实际的设置)。为了获得这部分扩展了的地址空间,用于创建进程的可执行文件必须在其文件头中标记了LARGEADDRESSAWARE链接器标志。如果没有,那地址空间还是限制为2GB。
  • 64位进程(自然是在64位的Windows系统上),地址空间大小为8TB(Windows 8和更早版本)或者128TB(Windows 8.1和以后版本)。
  • 64位Windows系统上的32位进程,如果映像文件链接时用了LARGEADDRESSAWARE标志,那么地址空间大小为4GB,否则,仍然是2GB。

000

LARGEADDRESSAWARE标志的需求来自这样一个事实,即2GB的地址空间只需要31位,因此可以把最高有效位(MSB)给应用程序使用。指明这个标志表示这个程序不会使用第31位,因此将此位设置为1(设置为1表示地址大于2GB)不会引起任何问题。

每个进程都有自己的地址空间,这就导致了进程内的任何地址都是相对地址,而不是绝对地址。例如,要试图搞清楚地址0x20000上有什么内容,光靠这个地址是不够的,还必须指明这个地址相关联的进程。

这些地址被称为虚拟地址,意思是地址范围与它实际在物理内存(RAM)中的位置之间是间接关系。进程中的一块缓冲区可能被映射到物理内存中,也可能临时存在于文件(比如页面文件)中。“虚拟”这个术语指出了这么一个事实,从代码执行的角度来看,没有必要知道将要访问的内存是不是在RAM里。如果内存确实被映射到了RAM里,CPU就可以直接访问数据;如果没有,CPU会产生一个页错(page fault)异常,内存管理器的页错异常处理程序会从适当的文件中读取数据,将数据复制到RAM里,对负责映射缓冲区的页表(page table)入口做相应的修改,并指示CPU重试访问。图1-3显示了两个进程中从虚拟内存到物理内存的映射。

000

图1-3 虚拟内存映射

内存以页面为单位进行管理。任何一种内存相关的属性,比如内存保护,都是以页面为粒度的。页面的大小由CPU的类型决定(在有些处理器里,是可以配置的),内存管理器必须顺应这个要求。在所有Windows支持的体系结构里,普通的(有时候称为小的)页面大小是4 KB。

除了普通的(小的)页面之外,Windows也支持大页面。大页面的大小是2MB(x86/x64/ARM64)和4 MB(ARM)。大页面直接使用页目录入口(Page Directory Entry,PDE)进行映射,而不使用页表进行映射。这会得到更快的转换速度,但最重要的是能更好地利用地址转换缓冲区(Translation Lookaside Buffer,TLB)—一个由CPU维护的近期转换页面的缓存。在使用大页面的情况下,单个TLB入口能比使用小页面映射更多的内存。

000

大页面的缺点是其内存需要在RAM里是连续的,这在内存紧张或者非常碎片化的时候会失败。另外,大页面始终是非分页的并且只能设置读/写保护。Windows 10和Server 2016及以后版本还支持1GB的巨型页面。当一次内存分配至少有1GB那么大,并且能够满足在RAM里连续时,系统会自动使用这样的巨型页面。

1.2.1 页状态

虚拟内存中的每个页面处于如下三种状态之一:

  • 空闲—这个页面未被分配,那里没有任何东西存在。对此页面的任何访问将引起一个访问违例(access violation)异常。一个新创建的进程的大多数页面都是空闲的。
  • 已提交—空闲的相反情形,已分配的页面在不违反页面保护属性时可以成功访问(例如,写到一个只读页面会引起访问违例)。已提交的页面通常被映射到RAM或者文件中(比如一个页面文件)。
  • 保留—页面未提交,但是保留了地址范围供以后可能发生的提交操作使用。从CPU的角度看,这与空闲是一样的—任何访问都会导致一个访问违例异常。但是随后通过VirtualAlloc函数(或者NtAllocateVirtualMemory,相应的原生API)进行新的内存分配时,如果没有指定特定地址的话,将不会在保留的地址范围中进行分配。一个经典的例子是在分配内存时利用保留内存维护连续的虚拟地址空间,这将在1.3节中讲述。

1.2.2 系统内存

地址空间的较低部分由进程使用。当某个线程正在执行时,它相关的进程地址空间从0到上节所描述的最高限都是可见的。但是,操作系统本身也必须驻留于某个地方—这个地方位于系统支持的地址空间的高端部分,如下所述:

  • 32位系统,没有配置成增加用户虚拟地址空间时,操作系统驻留于虚拟地址空间的高端2GB,从地址0x800000000xFFFFFFFF
  • 32位系统,配置成增加用户虚拟地址空间时,操作系统驻留于剩余的地址空间中。举个例子,如果系统配置成每个进程最大3 GB用户地址空间,操作系统就使用高端1 GB(从0xC00000000xFFFFFFFF)。从这样的地址空间缩减中最可能受到影响的是文件系统缓存。
  • Windows 8、Server 2012或更早版本的64位系统,操作系统使用高端8TB的虚拟地址空间。
  • Windows 8.1、Server 2012 R2及以后版本的64位系统,操作系统使用高端128TB的虚拟地址空间。

系统空间与进程是无关的—毕竟,在系统中,是同一个“系统”、同一个内核、同一个驱动程序在为每一个进程提供服务(有个例外,有部分系统内存是基于会话的,但是在现在的讨论中这不重要)。因此,系统空间中的任何地址都是绝对地址而不是相对地址,从任何一个进程上下文来“看”,它都是一样的。当然,从用户模式访问系统空间会导致一个访问违例异常。

系统空间是内核所在之处,硬件抽象层(Hardware Abstraction Layer,HAL)和内核驱动程序在加载之后也会驻留在这里。因此内核驱动程序也会自动受到保护,不能从用户模式直接访问。这也意味着,这些驱动程序的影响是系统范围的。例如,如果一个内核驱动程序产生了内存泄漏,这块内存即使在驱动程序卸载之后也得不到释放。另一方面,用户模式进程不会有超出进程生命周期的内存泄漏。内核会负责关闭并释放任何死进程内部的东西(所有句柄都会被关闭,所有内存都会被释放)。