1、大页面和小页面:虚拟地址空间被划分成以页面为单位,这是因为硬件内存管理单元在页面的粒度上,将虚拟地址转译为物理地址。Windows支持两种页面尺寸:大页面和小页面,根据处理器体系结构不同,实际尺寸值有所差异,如下图所示:
大页面的优势是,当引用同一页面内其他的数据时,地址转换速度更快。原因在于第一次引用一个大页面内的某个字节,会触发硬件的地址转译块查缓冲区TLB()把必要的信息放在其高速缓存中,以便可以转译任何引用到该页面内其他字节的地址访问。
为了在拥有查过2GB物理内存的系统上充分利用大页面的优势,Windows使用大页面来映射核心的操作系统映像以及核心的操作系统数据(如非换页池的初始部分以及一些描述了每个物理内存页面状态的数据结构)。对于IO空间请求,如果此请求满足大页面的长度和对齐条件,则Windows也会自动利用大页面来映射。最后,Windows也允许应用程序使用大页面来映射它们的映像、私有内存以及由页面文件支撑的内存区。
2、保留页面和提交页面:一个进程的虚拟地址空间中的页面有如下几种状态:空闲的、保留的、提交的或者共享的。访问提交的页面和共享页面会被转译到物理内存中的有效页面。提交的页面也称为私有的页面,因此提交的页面不能被其他进程共享。私有的页面是通过Windows的VirtualAlloc、VirtualAllocEx和VirtualAllocExNuma函数来分配的。这些函数允许一个线程保留一段地址空间,然后提交这个保留空间的多个部分。这个“保留的”中间状态允许线程把一段连续的虚拟地址用于将来可能的用途(如用于一个数组),同时又只占用可以忽略不计的系统资源,然后在应用程序运行过程中根据需要提交已保留空间的多个部分。试图访问空闲的或者保留的内存将会导致一个异常,因为这两类页面还没被映射到任何能够解析该引用操作的存储介质中。“私有的”页面指的是这些页面对于其他的进程通常是不可访问的。
注意:有一些函数,如ReadProcessMemory和WriteProcessMemory,它们显然运行跨进程来做内存访问,但是这些是由内核模式中运行的代码在目标进程中实现的(这叫“负载到目标进程上”)。它们也要求一下两个条件之一:目标进程的安全描述符分别被授予访问者PROCESS_VM_READ或PROCESS_VM_WRITE权限,或者访问者拥有SeDebugProvilege特权。
3、锁住内存:一般而言,最好是让内存管理器来决定页面是否需要驻留内存中。然而,可能在一些特殊的情况下,应用程序或者设备驱动程序必须将页面锁在物理内存中。可通过一下两种方式实现:1)应用程序可以调用VirtualLock函数来锁住其进程工作集中的页面,利用这种机智的所住的页面会一直驻留在内存中直到被显示地解锁或者锁住页面的进程终止为止。2)设备驱动程序可以调用内核模式函数MemProbeAndLockPages、MmLockPagableCodeSection、MmLockPagableDataSection或者MmPagableSectionByHandle。这种机制锁住的页面也会一直驻留在内存中直到显示地解锁。
4、共享文件和映射文件:对于多个进程可见的物理内存或者出现在多个进程虚拟地址空间中的物理内存被称为共享内存。例如,两个进程使用了同一个DLL,那么只需要将引用该DLL的代码页面加载到物理内存中一次,然后在所有映射了次DLL的进程之间共享这些物理页框即可,如下图所示。内存管理器中用来实现共享内存的底层原语是内存区对象,在Windows API中它也被称为文件映射对象。
5、内核模式堆:在系统初始化时,内存管理器创建了两种类型的动态大小的内存池(或者叫堆),大多数内核模式的组件从这两种内存池中分系统内存。
1)非换页池:它是由一些“可保证总是驻留在物理内存中”的虚拟地址范围构成的,由于这些地址范围总是驻留在内存中,因此任何时候从任何IRQL级别都可以访问它们,而不会招致页面错误(缺页异常)。之所以需要非换页池,其中一个根本原因就是:在DPC/Dispatch级别或者更高级别上,页面错误不可能被满足,因此,任何能在DPC/Dispatch级别或者更高级别上执行或者访问的代码和数据都必须存在于非换页内存中;
2)换页池:系统空间中的一段虚拟内存区域,它可以被换入和换出系统。凡是不需要从DPC/Dispatch级别或者更高级别上访问内存的设备的驱动程序可都可以使用换页池,在任何进程环境中它都是可以被访问的;
换页池和非换页池都位于系统地址空间部分,并且被映射到每个进程的虚拟地址空间中。Windows启动时,由4个换页池和1个非换页池,根据系统上NUMA节点数量的不同,将可能会创建更多的内存池,最多可达64个。
非换页池的初始大小是根据物理内存大小来确定的,并且它会根据需要进行增长,如下图所示:
6、虚拟地址空间的布局结构:每个进程都有一个私有的地址空间,其他的进程不能访问此地址空间。即,一个虚拟地址总是在当前进程的环境中被求值,因此它不能引用另外一个进程定义的地址。所以,进程内的线程永远不能访问私有地址空间以外的虚拟地址。即使是共享内存也不例外,因为共享的内存区是映射到每一个参与进来的进程中的,每个参与进程是通进程内的地址来访问的。同样的,跨进程的内存函数(ReadProcessMemory和WriteProcessMemeory)是由目标进程环境中运行的内核模式代码来操作的。每个进程都有自己的页表集合,这些页表被保存在只有从内核模式才能访问的页面中,因此一个进程的用户模式线程不能修改它们自己的地址空间布局。
7、系统空间:包含了全局的操作系统代码和数据结构,它们对于所有的进程都是可见的,而不依赖于当前正在执行的进程。系统空间是由下面的部分组成的:
1)系统代码:包含了操作系统映像、HAL,以及用于引导系统的设备驱动程序;
2)非换页内存池:不可换页的系统内存堆;
3)换页内存池:可换页的系统内存堆;
4)系统缓存:用于映射在系统级缓存中已经打开文件的虚拟地址空间;
5)系统页表项PTE:系统PTE池,其中的PTE被用于映射系统页面,如IO空间、内核栈以及内存描述符列表;
6)系统工作集列表:此工作集列表数据结构描述了系统的三个工作集(系统缓存工作集、换页池工作集和系统PTE工作集);
7)系统映射的视图:用于映射Win32.sys(即Windows子系统可加载的内核模式部分),以及系统使用的内核模式图形驱动程序;
8)超空间:用于映射进程工作集列表和一些其他的进程数据的特殊区域,这些数据并不需要在其他进程环境中被访问。超空间也被用于临时地将物理页面映射到系统空间中;
9)崩溃转储信息:保留的空间,用来记录有关一次系统崩溃的状态信息;
10)HAL使用:保留给予HAL相关的数据结构的系统内存;
8、x86虚拟地址转译:字节偏移量不会参与参与地址转译过程,也不会受地址转译的影响而改变它的值。它只是简单地从虚拟地址被复制到物理地址中,地址转译过程细节如下图:
x86地址转译过程步骤如下:
1)内存管理单元MMU使用一个特权模式的CPU寄存器CR3,来获取页目录的物理地址;
2)利用虚拟地址中的页目录索引部分,在页目录中索引定位到相应的页目录项PDE,该页目录项描述了映射此虚拟地址所需要的页表的位置,并包含了此页表的物理页面编号(也称为页面帧号PFN)。如果PDE中有个标志指示它描述了一个大页面,那么它就简单地包含目标大页面的PFN,其余的虚拟地址就被当做大页面中的字节偏移量来处理;
3)利用页表索引作为该物理页框中的一个索引,从而找到那个描述了目标虚拟页面所在的物理位置的PTE;
4)如果PTE中的“有效”位是空白的,则会触发一个页面异常(内存管理错误),操作系统的内存错误处理器找到该页面并试图将其变成有效的页面,然后进行第5步,如果该页面不能或者不应该变成有效位(如页面保护错误),则内存管理错误处理器产生一个访问违例或者错误检验。
5)当该PTE指向一个有效页面时(或者立即有效或者已经通过页面错误被解决),利用PTE中的PFN域,并利用原始虚拟地址中的字节偏移量,来构造出目标物理地址;
9、页目录:运行在非PAE模式下的X86系统上,每个进程都有一个页目录,这是内存管理器为了映射该进程中所有页表位置而创建的一个页面。进程页目录的物理地址保存在内核进程块(KPROCESS)中。在内核模式中运行的所有代码引用的都是虚拟地址而不是物理地址。
CPU从一个名为CR3的特权模式CPU寄存器中获取该页目录的位置,它包含了页目录的页面帧编号。每当发生不同进程间的线程切换时,内核中负责环境切换的例程,把将要运行的目标进程的KPROCESS块中页目录物理地址加载到该寄存器中。如果是同一进程内的线程切换则不需要重新加载页目录的物理地址。
每个页目录项都指向一个页表,一个页表就是一个包含PTE的数组。有效的PTE包含两个主域:物理页面帧号PFN(内存中一个页面的物理地址的PFN)和一些标志描述了该页面的状态和保护属性,如下图所示:
一旦内存管理器已经确定了物理页框编号,它还必须找到该页面内所要求的数据。这正是字节偏移量域的目的。初始的虚拟地址中的字节偏移量被简单地复制到物理地址中对应的域。在x86系统上,字节偏移量是12位宽,从而允许可以引用高达4096字节的数据(这是一个页面的大小)。换种解释,将虚拟地址中的字节偏移量连接到从PTE中获得的物理页框编号后面,就完成了从虚拟地址到物理地址的转译过程。
10、物理地址扩展PAE:通过适当的芯片集,PAE模式使得32位操作系统在当前的intel x86处理器上可以访问多达64GB的物理内存(没有PAE的话,最多4GB)。当处理器运行在PAE模式下时,内存管理单元MMU将常规页面映射位的虚拟地址分为4个域,如下图所示。MMU虽然实现了页目录和页表,但在PAE模式下,还有第三层在它们之上——页目录指针表。
每个页表或者页目录都必须存在于单个页中。
11、x64虚拟地址转译:x64上的地址转译机制类似于x86上的PAE,但是增加了第四级页表。当前实现的x64体系结构限制了虚拟地址的宽度位48位,其构成和转译示意图如下:
12、栈:一个线程在运行过程中,必须拥有一个临时的存储位置来保存函数的参数。局部变量和函数调用的返回地址,这部分内存叫做栈。在哪Windows上,内存管理器位每个线程提供了两个栈,分别为用户栈和内核栈,此外还痛了每个处理器上的栈,叫做DPC栈。用户栈通常有1MB大小,内核栈的大小要小得多:x86上是12kb,x64上是16kb。对于运行在内核中的代码,我们期望它比用户模式代码有更少的递归调用,同时能更充分地利用变量,并保持较小的栈缓冲区,因为内核栈位于系统地址空间中(通常是所有进程共享的),因此它们的内存使用会对整个系统带来较大的影响。