读书频道 > 系统 > windows > Windows内核原理与实现
4.2.1 系统地址空间初始化
2013-05-18 15:22:09     我来说两句 
收藏    我要投稿   

本文所属图书 > Windows内核原理与实现

本书从操作系统原理的角度,详细解析了Windows如何实现现代操作系统的各个关键部件,包括进程、线程、物理内存和虚拟内存的管理,Windows中的同步和并发性支持,以及Windows的I/O模型。在介绍这些关键部件时,本...  立即去当当网订购

4.2  Windows系统内存管理

在Intel x86 处理器的 Windows 系统中,0x80000000~0xffffffff是所有进程共享的系统地址空间。在这段地址空间中,其布局结构是在内核初始化阶段完成的。本节将首先介绍系统地址空间的初始化过程,然后描述系统地址空间中的动态内存管理算法。

4.2.1   系统地址空间初始化

在第2 章中,我们已经介绍过 Windows 的引导过程,在内核获得控制以前,Windows的加载程序(即ntldr )已经打开了 Intel x86 处理器的分页机制,并且预先建立了足够的页表以便16 MB 以下的低地址可以通过页表来访问其物理内存,也就是说,16 MB 以下的虚拟地址将直接映射到相同地址的物理内存上。因此,16 MB 地址以下的代码仍然以透明的方式在保护模式和分页机制下运行。而且,ntldr 在加载内核模块(ntoskrnl.exe )时将把它映射到特定的虚拟地址上,然后再把控制权交给其主函数KiSystemStartup 。

GDT的设置是在ntldr 中完成的,虽然WRK中没有这部分代码,但是,通过在调试器中跟踪WRK的启动代码,我们可以看到,在KiSystemStartup 函数获得控制时,段寄存器CS、DS、ES、SS和FS的值分别如下:

CS:0x8   —— 对应二进制表示1000

SS:0x10   —— 对应二进制表示1 0000

DS、ES:0x23 —— 对应二进制表示10 0011

FS:0x30   —— 对应二进制表示11 0000

而寄存器gdtr 的值为0x8003f000 。参照图 4. 5 关于段选择符格式的描述,我们可以知道,CS指向GDT中索引为1 的段,SS指向GDT中索引为2 的段,DS和ES指向GDT中索引为4 的段,FS指向GDT中索引为6 的段。根据 gdtr 的值,我们检查这些段的段描述符,如表 4. 1 所示。CS、DS、ES和SS段指向整个地址空间,从地址 0 一直到32位最大地址(差最后一个页面)。FS 指向一个特殊的页面,后面我们还会讲到,此页面包含了当前处理器的控制区(KPCR)信息。正因为如此,我们在系统代码中常常可以看到通过FS来获得当前处理器的全局信息,比如当前线程(参考3.4.2节)。

表4.1   Windows系统中用到的段描述符设置
段  描述符地址  描述符内容(4 个16位整数)段基地址  段最大偏移
CS 0x8003f008  ffff 0000 9b00 00cf  0x00000000  0xfffff000
SS 0x8003f010  ffff 0000 9300 00cf  0x00000000  0xfffff000
DS、ES  0x8003f020  ffff 0000 f 300 00cf  0x00000000  0xfffff000
FS  0x8003f030  0001 f000 93df  f f c0  0xffdff000  0x00001000

由于CS、DS、ES和SS段的这种设置方式,相当于段机制被屏蔽了,“段+ 偏移”形式的逻辑地址直接被映射成线性地址,这种做法也称为地址空间的平面化。因此,在Windows 中,所有的内存访问都是线性地址空间中的内存地址。段的设置无须特别考虑。
 
现在回到KiSystemStartup 函数,它调用KiInitializeKernel 函数进行内核初始化。KiInitializeKernel函数在P0处理器即引导处理器上,执行系统全局范围的内核初始化。KiInitializeKernel函数调用ExpInitializeExecutive,对执行体进行初始化。内存管理器是在执行体中初始化的。

在ExpInitializeExecutive函数(base\ntos\init\initos.c 文件的241~901 行)中,除了执行体自身被初始化以外,执行体的各个子组件,包括内存管理器,也被初始化。而且,每个子组件都被调用两次初始化,分别对应于阶段 0 初始化和阶段1 初始化。关于内核的两阶段初始化的完整介绍,请参考2.6.2节。图4.11 显示了与内存管理有关的初始化过程。


 

Windows 加载程序ntldr 只提供了必要的内存环境,系统空间的主要初始化工作是在MmInitSystem函数中完成的,因此,接下来我们看一看MmInitSystem 函数的代码(base\ntos\mm\mminit.c文件的 336~2 397行)。MmInitSystem 函数包括三部分代码逻辑,分别对应于阶段 0、阶段 1 和阶段 2 的初始化,如图 4.11 所示。这里阶段 1 与阶段 2 初始化都是在 Phase1InitializationDiscard 函数中被调用的。我们可以把它看做三个函数的简单组合。

MmInitSystem函数在阶段0 所做的初始化工作(434~2 208 行代码)主要是完成数据结构的初始化以及一些全局变量的设置。在 466~468 行,我们看到三个全局变量MmHighestUserAddress 、MmUserProbeAddress和MmSystemRangeStart的设置如下:

MmHighestUserAddress = (PVOID)(KSEG0_BASE - 0x10000 - 1);
MmUserProbeAddress = KSEG0_BASE - 0x10000;
MmSystemRangeStart = (PVOID)KSEG0_BASE;

这里KSEG0_BASE为0x80000000(见base\ntos\inc\i386.h的1 980 行),所以,用户地址空间(也称为进程地址空间)最高地址为0x7ffeffff ,而系统地址空间从0x80000000开始。接下来我们先看一下宏MiGetPteAddress:
#define MiGetPteAddress(va) ((PMMPTE)(((((ULONG)(va)) >> 12) << 2) + PTE_BASE))

这里PTE_BASE 的值为0xc0000000 (见base\ntos\inc\i386.h的1 972 行),因此,MiGetPteAddress的含义是,给定一个虚拟地址,计算出其对应的 PTE 的地址,即虚拟地址所在页面的页表项的地址。从该定义也可以看出,所有的页表项都按顺序存放在以0xc0000000 起始的内存处。再看宏MiGetPdeAddress:
#define MiGetPdeAddress(va)  ((PMMPTE)(((((ULONG)(va)) >> 22) << 2) + PDE_BASE))
 
这里PDE_BASE 的值为0xc0300000 或0xc0600000 ,取决于PAE (物理地址扩展)是否打开(见 base\ntos\inc\i386.h的1 964或1 968行)。我们不考虑 PAE 的情形,所以,PDE_BASE 的值为0xc0300000 ,即页目录项位于0xc0300000 处。

说明一点,ntldr 在将控制权交给内核以前,已经将内核、HAL和一些被标记为“引导-启动”的驱动程序映射到了0x80000000偏上的位置处(在Windows Server 2003 SP1系统中,此位置为0x80800000)。其中引导-启动的驱动程序稍后将被重定位到高端内存(系统PTE )区域。

回到MmInitSystem函数(见mminit.c的499 行)中,接下来设定系统视图(system view )大小为16 MB ,会话空间(session )的大小为 48 MB (见base\ntos\mm\mi.h 的8 261~8 317行)。会话空间直接位于 0xc0000000 之下的48 MB ,即0xbd000000~0xbfffffff。从minit.c
的582 行一直到754 行,计算会话空间和系统视图的内存位置,计算的结果以及对应的一些全局变量如图4. 12所示。在图中,会话映像文件区包括win32k.sys、视频驱动程序以及一些打印驱动程序的映像文件;会话内存池是指属于会话空间的换页内存池。


 

接下来MmInitSystem函数初始化系统缓存的位置,MmSystemCacheStart是个已初始化的全局变量,为0xc1000000 。系统缓存的结束位置是0xe1000000 ,宏MM_SYSTEM_ CACHE_END 包含此值。然后是换页内存池的变量设置,MmPagedPoolStart 的初值为0xe1000000 。换页内存池的大小为 MmSizeOfPagedPoolInBytes,默认 32 MB ,后面还要对它进行调整。

然后计算系统PTE 的数量(见 mminit.c 的1 173~1 287行)。MmInitSystem根据系统可用页面的数量,对MmNumberOfSystemPtes全局变量进行赋值,最少7 000个页面,最多50 000 个页面。如果要检验驱动程序的话,还需要额外的系统PTE (见mminit.c 的1 282~1 287行)。

接着初始化与堆内存管理有关的全局变量。然后有一个重要的函数调用:MiInitMachineDependent。此函数的主要功能是,真正让 Windows 的虚拟内存运转起来,前面介绍的 MmInitSystem 函数所做的工作只是在划分虚拟地址空间,并没有真正建立页目录项和页表。而MiInitMachineDependent真正建立页目录,以及建立页表来映射内核各个区域。特别是,非换页内存池也是在此函数中初始化的。关于该函数,我们稍后详细介绍。

然后MmInitSystem函数初始化跟物理内存相关的全局变量。首先调用MmInitializeMemoryLimits获得有关物理内存的基本信息。为了方便地管理物理内存,系统使用一个足够大的位图来记录物理页面的有效性。在完成了内存基本初始化以后,MmInitSystem调用MiReloadBootLoadedDrivers 函数,将ntldr 加载的引导-启动驱动程序重定位到系统PTE 区域,因而它们也能得到页面机制的保护,并有可能被换页。

接下来根据可用物理页面的数量来确定系统内存的规模,基本的规则是,小于19 MB ,则认为是小系统。对于工作站而言,在19~31 MB 之间,则认为是中等规模系统;大于等于32 MB ,则认为是大系统。对于服务器而言,在19~63 MB 之间,则认为是中等规模系统;大于等于 64 MB ,则认为是大系统。另外,MmInitSystem 也设置一组全局变量(见mminit.c的1 554~1 676行),包括MmMaximumDeadKernelStacks、MmModifiedPageMaximum、MmSystemCacheWsMinimum、MmDataClusterSize 、MmCodeClusterSize、MmReadClusterSize、MmInPageSupportMinimum、MmFreedExpansionPoolMaximum等。这些全局变量用于控制系统空间中一些特殊用途的物理内存页面的数量或者上下限。然后,调整可用物理内存页面的数量,即全局变量MmResidentAvailablePages,从当前可用页面数量中减掉32个保留页面(1 717行),再减掉非换页扩展所需要的页面数(1 725行,系统启动时物理页面数的1/6,最多不超过256 MB),还要减掉系统缓存所占用的最少物理页面数(1 731行)。

然后 MmInitSystem 函数建立起系统缓存结构(1 761~2 037 行),这其中的大部分代码都在处理多级页表(三级或四级),为简化起见,我们只考虑Intel x86 的二级页表机制。在WRK中,系统缓存结构的地址是0xc0c00000(全局变量 MmSystemCacheWorkingSetList),而系统缓存的地址是0xc1000000(全局变量MmSystemCacheStart),两者之间正好差4 MB,所以,这两个地址的PDE 正好是相邻的(见1 843 行的ASSERT )。系统缓存的最大可能地址是0xe1000000 ,但实际上其真正的结束地址可能会小一些,因而系统缓存的尺寸不会有0xe1000000~0xc1000000 那么大。读者可以在MmInitSystem 函数中跟踪MaximumSystemCacheSize 的变化情况。计算得到 MmSystemCacheEnd 和MmSizeOfSystemCacheInPages以后,就可以为这部分空间分配页表了(注意,这里仅仅分配页表而非页面)。最后调用 MiInitializeSystemCache初始化系统缓存,由它负责初始化系统缓存工作集,并建立起相应的管理数据结构。关于系统缓存空间的内存管理,请参考7.2.1节。

至此,MmInitSystem 函数已经基本上定义好系统空间了,并且完成了初步的初始化工作。现在设置全局变量 MmTotalCommitLimit 为一个真正有意义的值,它代表了最多可以提交的内存数量,即最多可以兑现多少物理内存。另外,全局变量MmTotalCommitLimitMaximum等于MmTotalCommitLimit。工作集的高限MmMaximumWorkingSetSize为总可用页面数减去512,当然它不可能超过2 GB。

然后 MmInitSystem 调用 MiBuildPagedPool函数,建立起换页内存池。在建立了换页内存池以后,就可以初始化已加载的模块表了(因为需要申请换页内存)。把这些模块映射到系统空间中。这是通过调用MiInitializeLoadedModuleList函数(2 099行)来完成的。
 
最后,若可用物理内存仍然超过127 MB,则增加更多的系统PTE ,让非换页区域后移(2 39~2 176行)。然后拷贝一份页目录,以便将来大页面系统PTE 映射被删除时(发生在MiUnmapLargePages 函数中),还可以恢复原始的页目录项。

在MmInitSystem 函数的阶段0 初始化部分,有相当一部分工作是在MiInitMachine- Dependent函数中完成的,包括非换页内存池的初始化工作和 PFN 数据库的初始化。为了理解物理内存的初始分配情况,有必要看一下这个函数,其代码位于base\ntos\mm\i386\ init386.c 的762~3 568 行。由于此函数的代码较多,这里只介绍与系统空间初始化相关的代码逻辑。并且,为简便起见,在下面解释代码的过程中,将不考虑与 MmVirtualBias变量非零(即/3GB 引导选项)、64位版本、PAE 支持和非对称内存模型相关的代码逻辑。
 
在854~973 行代码,MiInitMachineDependent处理大页面支持、变量初始化,以及生成页目录项和页表项的模板。然后设置当前进程的页目录,代码如下:
    PointerPte = MiGetPdeAddress (PDE_BASE);
    PdePageNumber = MI_GET_PAGE_FRAME_FROM_PTE (PointerPte);
 
    CurrentProcess = PsGetCurrentProcess ();
 
    DirBase = MI_GET_PAGE_FRAME_FROM_PTE (PointerPte) << PAGE_SHIFT;
 
    CurrentProcess->Pcb.DirectoryTableBase[0] = DirBase;
    KeSweepDcache (FALSE);

这里我们不考虑PAE 情形,KeSweepDcache 是个空语句,什么也不做。宏 MI_GET_ PAGE_FRAME_FROM_PTE直接从PTE 中提取出它的页帧编号,即物理地址。因此,进程对象的KPROCESS结构的DirectoryTableBase[0]记录了该进程的页目录物理内存地址。接下来1 020~1 023行代码把页目录表中对应于0~2 GB 之间的页目录项写成零,意味着当前进程(即空闲进程)不使用这部分空间。

然后MiInitMachineDependent 根据ntldr 传递进来的关于物理内存的描述符链表LOADER_PARAMETER_BLOCK::MemoryDescriptorListHead的信息,求出物理内存页面的数量,以及空闲物理内存的最低地址(见 1 030~1 083 行)。这样得到全局变量 MmNumber- OfPhysicalPages 和MmLowestPhysicalPage 的值。接着,设置全局变量 MmDynamicPfn 和MmHighestPossiblePhysicalPage,即最高可用的物理地址页编号和最高可能的物理页面。然后调整全局变量MmSizeOfNonPagedPoolInBytes和MmMaximumNonPagedPoolInBytes ,即非换页内存池的大小和最大尺寸(1 281~1 489行)。

接下来从1 499行代码开始,首先获得用于物理内存管理的辅助颜色值及其掩码,即全局变量 MmSecondaryColors和MmSecondaryColorMask 。然后计算 PFN 本身需要的非换页内存开销 MxPfnAllocation。由于非换页内存池是从高地址向低地址方向计算的,而非换页池的结束地址 MmNonPagedPoolEnd 等于 0xffbe0000,所以,其起始地址MmNonPagedPoolStart为结束地址减去最大可能的地址 MmMaximumNonPagedPoolInBytes,再加上当前大小,即MmSizeOfNonPagedPoolInBytes(见代码 1 589行),然后对 MmNonPagedPoolStart做对齐调整(见1 628行)。

在Windows 系统空间的内存布局中,系统 PTE 区域位于非换页内存池的紧前面,全局变量MmNonPagedSystemStart 记录了系统PTE 区域的开始地址,由于系统 PTE 区域和非换页内存池两者都是不可以被换出到外存中的,所以这两部分的页表都需要在初始化的时候建立好。首先计算 MmNonPagedSystemStart 的位置(见 1 730行),此地址不得低于MM_LOWEST_NONPAGED_SYSTEM_START,即0xeb000000 (见1 734 行)。另一方面,如果换页内存池的结束位置越过了系统PTE 的起始位置,则需要对换页内存池的大小作调整(见1 744~1 800行代码)。然后,检查内核映像和 HAL映像是否可以用大页面映射,如果满足条件,则对它们使用大页面映射(见代码1 808~2 033 行),记录在MiLargeVaRanges全局数组中。

接下来开始分配物理内存,考虑 PFN 数据库和非换页内存池所需要的页面(见 2 048
行代码中的 PagesNeeded变量)。尽可能地使用大页面映射,如有必要,减小非换页内存池
的大小以便能对齐到大页面(4 MB)边界。PFN 数据库的位置(全局变量MmPfnDatabase)
处于已加载的映像区(包括内核、HAL等)之上,并且对齐到大页面边界。如果从PFN 数
据库开始位置一直到系统视图之间的虚拟地址空间不足以容纳所需页面数量,则相应地减
小非换页内存池(见代码2 080~2 101 行)。2 133~2 225 行代码真正从物理内存描述符结构
MxFreeDescriptor中,扣除PFN 数据库和非换页内存池所需要的页面,记录在局部变量
FirstPfnDatabasePage 和PagesNeeded中,并且这部分空间对齐到大页面边界,加入到大页面
映射数组 MiLargeVaRanges 中。记录非换页内存池起始地址的全局变量
MmNonPagedPoolStart也随之更新为紧跟在 PFN 数据库之后(见代码 2 235行)。2 284~2 307行代码是当 PFN 数据库和非换页内存池不使用大页面映射时非换页内存池的分配逻辑。
 
接下来2 320~2 337行代码真正分配系统PTE 和扩展的非换页内存池地址范围的页表。代码如下:
StartPde = MiGetPdeAddress (MmNonPagedSystemStart);
EndPde = MiGetPdeAddress ((PVOID)((PCHAR)MmNonPagedPoolEnd - 1));
 
while (StartPde <= EndPde) {
 
    ASSERT (StartPde->u.Hard.Valid == 0);
 
    //
    // Map in a page table page, using the
    // slush descriptor if one exists.
    //
 
    TempPde.u.Hard.PageFrameNumber = MxGetNextPage (1, TRUE);
    *StartPde = TempPde;
    PointerPte = MiGetVirtualAddressMappedByPte (StartPde);
    RtlZeroMemory (PointerPte, PAGE_SIZE);
    StartPde += 1;
}

在这段代码中,首先确定从 MmNonPagedSystemStart开始到 MmNonPagedPoolEnd – 1的地址范围所对应的起始和结束 PDE :StartPde 和EndPde,然后分别填充其中的每一个页目录项。MxGetNextPage分配一个物理页,返回其页帧号,TempPde 中的 PageFrameNumber填好以后,即被赋给 StartPde 所对应的PDE 。MiGetVirtualAddressMappedByPte函数(实际上是一个C 语言宏)的功能是,根据一个PTE 项的地址(这里把StartPde 当做一个PTE来看待),计算出对应页表的虚拟地址,也就是说,获得刚刚分配的页表的虚拟地址,从而可以初始化此页表(填充零)。MiGetVirtualAddressMappedByPte的逻辑很简单,左移10位,但是,这里实际上用到了一个设计技巧,下面的插入文本解释了这一机制。

Windows的页目录自映射方案

由于Windows 的分页机制已经起作用,处理器的内存访问指令只认虚拟地址,不认物理地址,所以,为了访问刚刚分配的页表,即例子代码中MxGetNextPage 返回的那个页面,必须要经过PDE 和PTE 的查询,但是,我们现在只有物理页帧编号(PFN ),尚不知道哪个虚拟地址可以访问此页面。Intel x86 的内存访问要经过两次查表,参见图 4 . 3。并且,第二次查表的结果恰好指向目标页面。现在我们来看Windows 的页目录和页表结构如何使这一逆过程也非常高效和便捷。

Windows 的页目录地址(即PDE 的基地址,在代码中为宏PDE_BASE )是0xc0300000 ,PTE 的起始地址是0xc0000000 (在代码中为宏PTE_BASE ),并且 Intel x86 上的PDE 和PTE 的结构可以认为是相同的(见4.4.1 节的描述)。为了映射一个页表并将其中的内容清零,首先需要在页目录中增加一个PDE ,即赋值语句“*StartPde = TempPde; ”所做的事情,其次,要能够以虚拟地址方式来访问此页表。实际上,对于页目录中的PDE 本身的访问也需要经过两次查表,即赋值语句中的“*StartPde ”其实已经在访问页目录页面的内容了。

PDE 的查表是根据 CR3 寄存器和虚拟地址高 10 位来确定的,因此,一旦 CR3 寄存器已经置值,则第一次查表便已确定。关键是第二次查表。Windows 做了一个巧妙的设计,让一个页表的地址范围(4 MB 大小)具有某种结构,使得对页表页面的访问可直接利用上一级对它引用的虚拟地址来导出,即根据PDE 或PTE 的虚拟地址来导出它所指的物理页面的虚拟地址(MiGetVirtualAddressMappedByPte或MiGetVirtualAddressMappedByPde)。

方案是这样的:假设页表的 PDE 的虚拟地址已知(即例子代码中的 StartPde),为了构造页表页面的虚拟地址,我们这样设计两次查表:第一次,查找页目录本身的页面;第二次,查找刚刚增加的那个 PDE (实际上,这次要把 PDE 当做PTE 来看待)。基于这一思路,我们得到这样的虚拟地址:最高10位与页目录虚拟地址的高10位相同,任何一个PDE 都满足此条件,接下来的10位应该是StartPde 与页目录基地址之间的差除以4,即两者之间的PDE 数量。由于页目录地址0xc0300000 最高10位与紧接着的10位都是1100000000b ,所以,StartPde 左移10 位恰好满足刚才提到的两个条件。这正是MiGetVirtualAddressMappedByPte如此简单的原因,并且 PDE_BASE 取值0xc0300000 也使得任何一个PDE 左移10位以后其对应的PDE 仍然不变。图4. 13显示了页目录与页表之间的关系,注意,页目录中的 1100000000b 项(十进制 768)指向页目录本身。图中分别演示了StartPde 作为一个PDE 以及地址p 作为一个页表页面的地址转译过程。读者可以自行验证,0xc0300c00 中的页帧编号与 CR3 寄存器中的页帧编号是相同的。实际上,从这里我们也可以看出,一旦确定了0xc0300c00 作为页目录地址以后,整个虚拟地址空间的所有页表自然位于0xc0000000~0xc0400000 之间。


 

这种方案的关键之处在于,页目录的 0xc00 项指向它自己,并且 PDE 和PTE 的格式是兼容的,所以,页目录中的每一个PDE 在用于访问页表页面时也被当做PTE 来映射。因此,这种方案被称为页目录自映射方案。

进一步可以推广,在多级页表结构中,从任何一级PTE 都可以利用同样的左移位方法获得其所指的物理页面的虚拟地址。例如,在图 4. 13中,对于页表中的任何一个 PTE ,它所指的物理页面的虚拟地址都同样可以用MiGetVirtualAddressMappedByPte来实现。请读者自行验证。此外,MiGetVirtualAddressMappedByPde 的实现更加直截了当,将PDE左移20位代表了它所映射的虚拟地址起始位置,大小为4 MB。

接下来的代码 2 341~2 484 行分配 PFN 数据库和非换页内存池,一直到系统视图开始处这段地址范围的页表,如果在非换页内存池与系统视图之间有空隙(至少容得下一个PDE ,即4 MB 大小),则这段空隙用做额外的系统缓存和系统PTE (参见 7.2.1 节)。此外,如果1 GB~2 GB 范围也被用做系统空间,则为指定的范围,从 MiUseMaximumSystemSpace到MiUseMaximumSystemSpaceEnd ,分配相应的页表。注意,这两个全局变量在MmInitSystem函数中已经根据系统的引导参数而赋值了。

Windows 在系统地址空间高端处保留了一块地址范围用做系统崩溃时的信息转储区域,其起始地址为 0xffbe0000 ,2 497~2 509行代码为这块内存(不超过 4 MB)分配了一个页表。

接下来从2 515 一直到2 564 行代码,为非换页内存池分配页面,这些页面前面已经预留了,并且页帧编号是连续的。然后在2 579行调用MiInitializeNonPagedPool 函数建立起非换页内存池的管理结构,在2 581 行调用 MiInitializeNonPagedPoolThresholds函数设定非换页内存池的高、低阈值。

然后分配PFN 数据库所需要的物理页面。在使用大页面的情况下,这些页面前面已经预留了,从FirstPfnDatabasePage 开始,总共MxPfnAllocation个页面,见代码2 590~2 624行。在不使用大页面的情况下,所需页面尚未预留,需要保证为每一个物理页面对应一个MMPFN结构(其定义见base\ntos\mm\mi.h 中,共24个字节长)。PFN 数据库实际上是一个位于内核映像之后的MMPFN 数组,所以,这部分代码为此数组分配物理页面,见代码2 628~2 728行。
 
接下来 2 752~2 783 行代码,为按颜色的空闲页面链表数组MmFreePagesByColor本身(并非链表)分配物理页面并初始化其内部链表结构。这里MmFreePagesByColor数组用于管理物理页面,参见 4.5.3 节。

2 790~2 900行代码为所有已设置为有效(valid)的页面,更新一下对应的 PFN 数据库元素的状态。这也算是 PFN 数据库的初始化,这样,物理页面的分配便跟 PFN 数据库中的状态对应起来了。

接下来检查最低的物理内存页面,看它是否为 0,如果为 0 并且当前尚未被使用,则将它标记为已经在使用。这样做是为了查找“物理页面被指定为零”的软件错误(bug),见代码2 908~2 928行。

从2 964行开始,根据系统加载程序传递过来的物理内存描述符链表,检查所有的内存描述符,并把可用的内存都加入到PFN 数据库的空闲链表中。由于加载程序传递过来的链表是从低地址到高地址排序的,而这里的循环检查按相反的顺序,因而高地址的可用物理内存先插入到空闲链表中,低地址的内存后插入到链表中。这种做法的考虑是,系统刚开始时分配的内存不太可能被释放,因而低地址的物理内存(比如最低的16 MB )可以为某些特殊的设备或驱动程序使用。

接下来扫描PFN 数据库所用到的PTE ,并把它们对应的页面标记为已经在使用,即在数据库中将引用计数加1。代码见3 105~3 239行。

然后调用InitializePool 函数初始化非换页内存池,见代码 3 263行。至此,非换页内存池已经建立起来,可供使用了。

接下来初始化系统 PTE 池,这些 PTE 主要用于映射 I/O 空间、驱动程序的映像,以及内核栈。系统 PTE 池的范围从高端的非换页区域(由全局变量MmNonPagedSystemStart指定),一直到高端的非换页内存池(0xffbe0000 前面的那一段非换页内存池)。这段地址范围为系统PTE 区域。在代码3 295 行调用MiInitializeSystemPtes 函数对系统PTE池进行初始化。

如果前面在分配虚拟地址空间时已经分配了额外的系统PTE 区域,则将额外的区域加入到系统PTE 池中。见代码3 302~3 346行。

最后,初始化当前进程的内存管理结构,并建立起工作集链表,这涉及创建一个PDE来映射超空间(位于0xc0400000 ),并初始化该PDE 所指的页表(见代码3 359~3 379行)。超空间建立起来以后,初始化相应的变量,包括超空间中管理PTE 的全局变量,以及进程的工作集链表(见代码3 385~3 390行)。然后为零页面线程保留用于零化操作的PTE (见代码3 396行)。接着为进程创建 VAD位图(见代码 3 412~3 430行),关于进程地址空间的VAD位图,请参考4.3.2节。之后,设置页目录的PFN 属性(见代码3 475~ 3 480行),以及为工作集链表分配一个页面(见代码 3 500~3 514行),然后,设置工作集的属性(见代码3 524~3 528行)。最后,把管理内存所需要的物理页面标记为已在使用(见代码3 534~3 565行)。

至此,MiInitMachineDependent函数完成其初始化工作,并返回。

经过了阶段0 初始化以后,根据MmInitSystem和MiInitMachineDependent两个函数所做的事情,系统地址空间的初始化已经完成。其布局结构已完全建立起来,而且,非换页内存区域已经分配好页表和页面,完整的内存结构如图4. 14所示。有关的全局变量如表4. 2 所列,表中也给出了在Windows Server 2003 SP1 一个典型系统配置(512 MB 内存)下这些变量的值。

表4.2   Windows系统中用于系统空间管理的一些全局变量
全局变量名  典型取值  全局变量名  典型取值
MmHighestUserAddress 0x7ffeffff  MiSessionImageStart 0xbf800000
MmUserProbeAddress 0x7fff0000  MiSessionImageEnd 0xc0000000
MmSystemRangeStart 0x80000000  MmSystemPteBase 0xc0000000
MmPfnDatabase 0x81000000  MmWorkingSetList 0xc0502000
MmNonPagedPoolStart 0x81301000  MmHyperSpaceEnd 0xc0bfffff
MmNonPagedPoolEnd0 0x82000000  MmSystemCacheWorkingSetList 0xc0c00000
MiSystemCacheStartExtra 0x82000000  MmSystemCacheStart 0xc1000000
MiMaximumSystemCacheSizeExtra 0x2f800  MmPagedPoolStart 0xe1000000
MiSystemViewStart 0xbb000000  MmPagedPoolEnd 0xf0bfffff
MmSessionBase 0xbc000000  MmNonPagedSystemStart 0xf0c00000
MiSessionPoolStart 0xbc000000  MmNonPagedPoolExpansionStart 0xf8ba0000
MiSessionViewStart 0xbc400000  MmNonPagedPoolEnd 0xffbe0000
MiSessionSpaceWs 0xbf400000  MmNumberOfPhysicalPages 0x0001ff7a

现在我们来看Windows 系统的阶段1 初始化过程,其中内存管理的初始化也是在MmInitSystem函数中完成的,见base\ntos\mm\mminit.c 文件中的2 211~2 389行代码。MmInitSystem再次调用MiInitMachineDependent函数,不过,这次MiInitMachineDependent函数只做了很简单的一点事情(见base\ntos\mm\i386\init386.c 文件中的863~870 行代码),即,如果处理器支持大页面,并且在前面初始化过程中有记录下来要转换成大页面的内存区域(在全局数组MiLargeVaRanges 中),则把这些区域转换成大页面映射,其中可能包括的区域为内核本身的映像区(包括内核和HAL)、PFN 数据库,以及非换页内存池。

然后,MmInitSystem调用 MiMapBBTMemory函数,此函数的用意是为 BBT (Basic Block Tools,一种代码优化技术)预留一块内存缓冲区。由于在默认配置下,全局变量BBTPagesToReserve 为0,所以,此函数实际上什么也不做,直接返回。接下来MmInitSystem调用MiSectionInitialization 函数创建内存区对象类型,创建段解引用系统线程(dereferencing segment thread ),以及创建内存区对象“\\Device\\PhysicalMemory ”,再插入到当前进程(System 进程)句柄表中。

接下来 MmInitSystem 从换页内存池申请一个PTE 对象,由全局变量MmSharedUserDataPte指向此 PTE 对象。该PTE 记录了用户共享数据页的 PTE ,对应的页面在内核和执行体初始化以前已分配好,位于0xffdf0000 处。进一步,把该PTE 赋到进程地址空间的0x7ffe0000 地址所对应的PTE 中,这样就实现了在 0xffdf0000 和0x7ffe0000 这两个地址映射到同一个物理页面上。因而,进程地址空间和系统地址空间共享同一个页面,从而用户模式代码可以访问系统中的一些状态数据(参考KUSER_ SHARED_DATA数据结构)。参见代码2 236~2 289行。

然后MmInitSystem 调用MiSessionWideInitializeAddresses、MiInitializeSessionWs- Support和MiInitializeSessionIds函数初始化会话空间。接着,MmInitSystem创建一个系统线程,称为修改页面写出器(modified page writer),见代码2 299~2 310行。然后,初始化系统内存事件,包括高内存和低内存事件,以及换页内存池和非换页内存池的高低事件。这是由MmInitSystem调用MiInitializeMemoryEvents函数来完成的,见2 317行代码。

接下来MmInitSystem启动平衡集管理器,这是两个系统线程,分别为KeBalanceSetManager和KeSwapProcessOrStack 函数。前者定期检查并管理进程的工作集;后者控制进程和内核栈的换入和换出。本章4.5和4.6节将进一步介绍这两个线程。

然后MmInitSystem调用MiStartZeroPageWorkers 函数,启动零化页面的系统辅助线程(它们仅负责在初始化阶段零化页面)。接下来是一个循环,对所有已加载的模块,调用MiWriteProtectSystemImage 函数以设置保护属性。见代码2 362~2 382行。

至此,阶段1 初始化完成,全局变量MiFullyInitialized置成 1。

在阶段2 初始化过程中,MmInitSystem仅仅调用MiEnablePagingTheExecutive函数(其代码位于base\ntos\mm\mminit.c 中4 247~4 664行)。此函数使当前已加载的模块中的可换页代码区变成可换页的,它定位到每个模块的PAGE 内存区,然后调用MiEnablePagingOfDriverAtInit 函数(其代码在base\ntos\mm\mminit.c 中4 667~4 800行)以改变相应PTE 中的标记。原来的 PTE 是由系统加载程序ntldr 设置的,当时内存管理器尚未建立起来,而现在系统空间已经完成初始化,所以,作为内存管理器初始化过程的最后一步,MmInitSystem对系统空间中可以换页的代码区,修改其PTE 使之能在内存紧缺的情况下腾出物理内存页面。

到这里,内存管理器的初始化工作全部完成,而且系统空间已经完全建立起来。此后执行的系统代码可以充分利用系统提供的内存管理功能,尤其是非换页内存池和换页内存池都已经可以正常使用了。

从以上的代码分析可以看出,Windows 的系统地址空间0x80000000~0xffffffff是经过精心安排的,一组全局变量规定了各个区域的范围。阶段0 初始化的主要职责是划分系统地址空间,它充分考虑到了物理内存的数量和引导选项指定的要求,使得最终得到的各部分区域的范围相对比较合理;阶段 0 也负责建立起页目录和页表结构,并且完成非换页区域的物理页面分配。阶段 1 初始化主要集中在一些内存管理任务的初始化,包括创建修改页面写出器和平衡集管理器,以及页面零化任务等。阶段2 初始化只是简单地使系统模块中的换页代码区可被换页而已。

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:4.1.4 Windows内存管理概述
下一篇:4.2.2 系统地址空间内存管理
相关文章
图文推荐
3.4.4 进程生命期管
3.4.2 Windows应用商
3.4.1 Windows应用商
3.4 进程生命期管理
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站