二、内存管理
操作系统的项基本功能就是管理物理内存,Windows NT系统通过IA32提供的虚拟内存机制为每一个进程提供了一个独立的、统一的虚拟内存空间。同时,系统负责对物理内存的管理和虚拟内存-物理内存之间映射关系的维护。
2.1物理内存管理
Windows NT系统对物理内存的管理并不是本文要介绍的内容,但由于和其它部分有所关联,因此在此之上简要得介绍一下相关的数据结构。
系统使用一个MMPFN结构的数组MmPfnDataBasee来描述每一物理内存页的情况。通过页帧号作为索引,可以很方便地使用MmPfnDataBasee数组查看物理页的情况,并找到相关的页表项以完成页交换。
大致的内存管理过程可以参考图2-0。
该结构的定义如下:
typedef struct _MMPFN {
union {
ULONG Flink;
ULONG WsIndex;
PKEVENT Event;
NTSTATUS ReadStatus;
struct _MMPFN *NextStackPfn;
} u1;
PMMPTE PteAddress;
union {
ULONG Blink;
ULONG ShareCount;
ULONG SecondaryColorFlink;
} u2;
union {
MMPFNENTRY e1;
struct {
USHORT ShortFlags;
USHORT ReferenceCount;
} e2;
} u3;
MMPTE OriginalPte;
ULONG PteFrame;
} MMPFN;
typedef MMPFN *PMMPFN;
什么是页帧号?这个结构在不同的情况下定义不同,可以理解为地址除去页内偏移后剩下的高位。地址转译的过程也和页帧号密切相关:通过虚拟地址中的各部分作为索引查表,得到最终的物理页帧号(这样就得到了物理地址中除了低12位页内偏移的其它部分),最终再以页内偏移为低位,就能得到物理地址) 2.2进程内存管理
现代操作系统通过“虚拟内存”机制为进程提供了独立的地址空间来防止相互干扰,并屏蔽了实际物理内存容量对应用程序的影响。应用程序面对的是统一的、完整的4GB地址空间,具体能使用的物理内存容量则由操作系统决定。
2.2.1平坦(FLAT)模式
运行在IA32架构下时,Windows NT系统打开了CPU的分页支持(并选择4KB小页面),但此时CPU的段机制仍然起作用。Windows NT通过特殊的机制,从而实现对段机制的屏蔽(平坦化)。
系统在启动后,将除FS外的所有的段描述符都设置为基地址为0,大小为0xffffffff,相当于每个段都指向了整个地址空间,所以无特殊情况下不用考虑纠结的段问题。但系统模式下和用户模式下段描述符依然有差异,主要是由于权限标志有所不同。至于FS段,指向了一个特殊的页面,主要用于异常处理,这里不作讨论。
通过这种“平坦”模式,段机制等于被屏蔽了,数据段和代码段以及栈都处在同一片地址区域中。应用程序的编写者无需考虑跨段访问等问题。
2.2.2进程地址空间
系统提供的进程地址空间为0x00000000-0x7fffffff,但这段空间中还有一些保留的区域,无法使用。
其中,0x00000000-0x0000FFFF为空指针保留区(为了使程序在使用空指针能产生异常以发现错误),0x7FFF0000-0x7FFFFFFF也被系统保留。其它部分可由用户程序使用,使用前需调用相关系统服务对对应的地址区域进行保留、提交操作。系统会根据参数修改对应的页表并分配物理页或者页文件。
在开启/3GB开关后,进程可访问的内存区域扩大到3GB,并且最后一块保留区域也相应地后移。
2.2.3用户层的虚拟内存管理
虽然页表在用户层无法访问的地址空间中,用户层无法操作,但NT系统提供了一系列API函数用于用户层的内存管理。
这些API通过调用系统服务切换到内核层后,以操作页表的方式来完成虚拟内存分配、权限设置等。应用程序虽然不能直接操作页表,但通过内存管理API依旧能完成大多数内存管理任务。
关于用户层的内存管理API不是本文的内容,可以参考《Windows核心编程》。
2.3 页表与地址转译
2.3.1页表与地址转译1:非PAE模式下
要完成虚拟内存到物理内存地址的翻译工作,就必须通过页表来实现映射。
如果直接使用单级的页表,一共需要4G/4K=1M=1048576个表项,在32位处理器上其占用内存空间为4MB(还算进了一些标志位占用的空间)。在考虑到每个进程都可能有一整份属于自己的页表,这样的内存开销即使对于现在的计算机仍然难以负担。所以Windows NT使用了多级页表机制。
对于32位Windows NT系统,使用2级页表,在打开了PAE(物理地址扩展,此功能在Pentium Pro处理器开始提供)后,则使用三级页表。
先来看不启用PAE的情况:
系统为每一个虚拟地址空间提供了一份PDE(页目录),其中包含1024个页目录项,每个页目录项指向一份页表,其包含的项数也是1024,每项内容为最终的物理地址的高20位。所以在转译地址时,CPU首先根据CR3寄存器的值获得页目录的基地址,然后由虚拟地址的高10(22-31位)为找到对应的页目录项,得到对应的页表的基地址在根据虚拟地址的中间10位(12-21位)在此页表中查找到最终的物理页基地址,最后,由此基地址加上虚拟地址中剩下的低12位(作为页偏移),得到最终的物理地址。
对于未启动PAE的IA32处理器,其PTE定义如下:
typedef struct _MMPTE_HARDWARE {
ULONG Valid : 1;
#if defined(NT_UP)
ULONG Write : 1; // UP version
#else
ULONG Writable : 1; // changed for MP version
#endif
ULONG Owner : 1;
ULONG WriteThrough : 1;
ULONG CacheDisable : 1;
ULONG Accessed : 1;
ULONG Dirty : 1;
ULONG LargePage : 1;
ULONG Global : 1;
ULONG CopyOnWrite : 1; // software field
ULONG Prototype : 1; // software field
#if defined(NT_UP)
ULONG reserved : 1; // software field
#else
ULONG Write : 1; // software field - MP change
#endif
ULONG PageFrameNumber : 20;
} MMPTE_HARDWARE, *PMMPTE_HARDWARE;
PDE与其定义相同。
但值得注意的是,LargePage位只有PDE启用,对于PTE无效。
PDE/PTE的结构中,最需要关注的就是其中的PageFrameNumber域,即页帧号。这里存储着转译过程中的下一级页表的虚拟地址的高位(低位用0填充)(对于PDE),或是物理页帧号的高位(对于PTE)。 具体关于PTE中各位的作用,请参阅《IA-32 架构软件开发人员手册》第三卷中的相关部分(
XXXXXXXXXXXXXXXXXXXXXXXXX/products/processor/manual/325384.pdf)。
需要注意的是,只有当Valid位(有效位)被设置时(即PTE的第0位=1)时,该PTE对应的页在物理内存中,反之则需要进行页交换(需要从磁盘上的页面文件读取页内容并写入内存,同时在此之前可能需要将一个在内存中的页写入磁盘以腾出内存空间)。
对于PDE,上述法则依然适用。
非PAE模式下的地址转译过程可以参考图2-1:
2.3.2页面与地址转译:PAE模式下
由于现代计算机内存容量的迅猛增长,Intel在Pentium Pro处理器开始提供36根地址线(即提供36位物理寻址支持)并加入了PAE(物理地址扩展)的支持。开启PAE模式后(通过在控制寄存器CR4的第五位进行设置/检测),整个地址转译机制发生了一系列变化。
首先,Windows NT在PAE模式下采用三级页表,新增加了一个页目录指针索引项(高2位,即30-31位),PDE和PTE的索引均变为9位。
另外PDE和PTE的结构也发生了改变,具体如下:
typedef struct _X86PAE_HARDWARE_PDE {
union {
struct _X86PAE_HARDWARE_PTE Pte;
struct {
ULONGLONG Valid : 1;
ULONGLONG Write : 1;
ULONGLONG Owner : 1;
ULONGLONG WriteThrough : 1;
ULONGLONG CacheDisable : 1;
ULONGLONG Accessed : 1;
ULONGLONG Dirty : 1;
ULONGLONG LargePage : 1;
ULONGLONG Global : 1;
ULONGLONG CopyOnWrite : 1;
ULONGLONG Prototype : 1;
ULONGLONG reserved0 : 1;
ULONGLONG reserved2 : 9;
ULONGLONG PageFrameNumber : 15;
ULONGLONG reserved1 : 28;
} Large;
ULONGLONG QuadPart;
};
} X86PAE_HARDWARE_PDE;
typedef struct _X86PAE_HARDWARE_PTE {
union {
struct {
ULONGLONG Valid : 1;
ULONGLONG Write : 1;
ULONGLONG Owner : 1;
ULONGLONG WriteThrough : 1;
ULONGLONG CacheDisable : 1;
ULONGLONG Accessed : 1;
ULONGLONG Dirty : 1;
ULONGLONG LargePage : 1;
ULONGLONG Global : 1;
ULONGLONG CopyOnWrite : 1; // software field
ULONGLONG Prototype : 1; // software field
ULONGLONG reserved0 : 1; // software field
ULONGLONG PageFrameNumber : 24;
ULONGLONG reserved1 : 28; // software field
};
struct {
ULONG LowPart;
ULONG HighPart;
};
};
} X86PAE_HARDWARE_PTE, *PX86PAE_HARDWARE_PTE;
typedef X86PAE_HARDWARE_PTE X86PAE_HARDWARE_PDPTE;
处理器在进行地址转译时,首先由CR3寄存器得到页目录指针表基地址(CR3的高27位,注意开启PAE时CR3定义的变化),在由虚拟地址中的页目录指针表索引(高2位,30-31位)查表得到PDE的基地址,
然后以虚拟地址中的PDE索引(9位,21-29位)查得PTE基地址,再以虚拟地址中的PTE索引(9位,12-20位)查表得到对应的PTE项,以其中的页帧号(PageFrameNumber,24位)加上虚拟地址中的页偏移(0-11位)得到最终的物理地址(36位)。
关于页目录指针(PDPTE),未能找到其相关结构,倒是发现了其定义:
typedef X86PAE_HARDWARE_PTE X86PAE_HARDWARE_PDPTE;
由此可推断,其大小为64位,结构与PAE模式下的PTE相同。在调试中也发现,其高32位总是0。
PAE模式下的地址转译可以参考图2-2:
综上所述,在PAE模式下,地址转译发生了很大的变化,并且物理地址用64位量进行描述(虽然其为36位),且PDE和PTE的结构不再保持相同并各自有所变化。
2.3内核空间内存的一致性
Windows NT中虽然每个进程都有一份独立的页表(或者说是页目录/页目录指针)以为每个进程提供独立的地址空间(这样可以实现所谓的“进程隔离”:各个进程之间的数据相互独立),但对于内核空间,则保持地址的一致性(即各进程中属于内核的空间是共享的),操作系统通过在为新进程建立地址空间是拷贝同一份PDE来实现。
这样做的理由为了方便工作在内核层的代码直接的相互访问,或许,微软认为能在内核执行的代码一定是规范的、安全的和可信任的(实际往往不一定)。
2.4内存池,非换页内存
内存池不是本文要讨论的话题,但这里指出,一部分内存属于非换页内存池,即该部分的内存始终不会被交换到页文件中,这里往往存储这那些需要保证能被访问到的关键数据,比如页表,或者其它需要常驻内存的代码。