读书频道 > 系统 > windows > Windows内核原理与实现
4.2.3 系统PTE 区域的管理
2013-05-18 15:50:51     我来说两句 
收藏    我要投稿   

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

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

上一节介绍了系统内存池和执行体内存池的内存管理机制,在Windows 的整个地址空间中,除了会话空间部分以外,还有一部分内存区也是以动态内存的方式来管理的,这就是系统 PTE区域,其起始地址由全局变量MmNonPagedSystemStart指定,而结束地址正好在MmNonPagedPoolExpansionStart之前。另外,在系统视图和非换页内存池之间若有空隙,则这段地址范围的全部或一部分(1/6)也用作系统 PTE 区域,称为系统 PTE 额外区,如图4.14 中所示;而前者以 MmNonPagedSystemStart开始的区域称为系统 PTE 基本区。Windows将这两部分区域链接在一起进行管理,因此,在本节下文中,我们不区分系统PTE 区域和额外区,而统称为系统 PTE 区域。

系统 PTE 区域是以 PTE 的形式来管理的,当内核代码需要一段虚拟地址来映射物理页面时,它可以使用系统 PTE 区域中的地址范围;当内核代码需要额外的非换页区域时,它也可以使用系统 PTE 区域来映射物理内存。有一点请注意,这部分虚拟地址范围称为系统PTE 区域,并非表明这段地址范围存放的是PTE ,而表示这段地址范围是以 PTE 的形式来管理的,即把 PTE 当做资源来管理。这一节我们来讨论系统PTE 区域的管理算法和实现。另外,非换页内存池的扩展区域也是通过PTE 来管理的,所以,我们在代码中会看到有两部分区域,这是通过C 枚举类型SystemPtePoolType 来区分的。在本节中,我们只关心SystemPteSpace 类型的 PTE 区域。

系统PTE 区域中页面的PTE 也同样位于系统地址空间的页表区域,即从 0xc0000000开始的区域,只不过,这些PTE 未必按照硬件所定义的 PTE 格式来解释。对于空闲页面的PTE ,它们并没有对应的物理页面,其PTE 被按照以下的数据结构来解释:
typedef struct _MMPTE_LIST {
    ULONG Valid : 1;
    ULONG OneEntry : 1;
    ULONG filler0 : 8;
    ULONG Prototype : 1;            // MUST BE ZERO 
    ULONG filler1 : 1;
    ULONG NextEntry : 20;
} MMPTE_LIST; 

从MMPTE_LIST结构的定义可以看出,硬件PTE 中的 PFN 域(见 4.4.1 节)被定义成了NextEntry,因此,可以想见,PTE 是按照链表的方式来管理的。因为PTE 对应的页面地址一定是页面对齐的,即最后 12 位总是 0,所以,这里只用 20 位就能表达系统 PTE 区域中的一个页面地址。在接下来的描述中,我们把通过PTE 建立起来的链表称为 PTE 链表。

在系统PTE 区域的内存管理中,除了 PTE 链表以外,还有 PTE 簇的概念,即连续地址的页面构成一个簇。PTE 链表只把簇头的PTE 连接起来,簇的大小是这样来规定的:如果一个簇只有一个页面,则它的OneEntry 位域为1;如果有多个页面,则第二个页面PTE 中的NextEntry项包含了页面数量,也即PTE 的数量。图4. 19显示了PTE 链表和PTE簇。其中浅灰色PTE 表示不在空闲链表中,说明已经被保留了。每个PTE 的第1 位为OneEntry 位,高 20位为NextEntry。图中显示的 PTE 链表的前三个节点分别包含2 个、1个和5 个PTE ,所以,第二个PTE 簇的 OneEntry 位被置 1。最后一个 PTE 簇包含 3 个PTE ,它的NextEntry为−1,即0xfffff 。


 

接下来看系统PTE 区域的PTE 管理算法。以下是一组全局变量定义:
#define MM_SYS_PTE_TABLES_MAX 5
#define MM_PTE_TABLE_LIMIT 16
ULONG MmSysPteIndex[MM_SYS_PTE_TABLES_MAX] = {1,2,4,8,MM_PTE_TABLE_LIMIT};
UCHAR MmSysPteTables[MM_PTE_TABLE_LIMIT+1] = {0,0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4};
 
ULONG MmSysPteMinimumFree [MM_SYS_PTE_TABLES_MAX] = {100,50,30,20,20};
 
PVOID MiSystemPteNBHead[MM_SYS_PTE_TABLES_MAX];
ULONG MmSysPteListBySizeCount [MM_SYS_PTE_TABLES_MAX];
 
ULONG MmTotalFreeSystemPtes[MaximumPtePoolTypes];
PMMPTE MmSystemPtesStart[MaximumPtePoolTypes];
PMMPTE MmSystemPtesEnd[MaximumPtePoolTypes];
MMPTE MmFirstFreeSystemPte[MaximumPtePoolTypes]; 

数组MmSysPteIndex 定义了5 种大小的连续页面内存块:1、2、4、8 或16,数组MmSysPteTables则是数组MmSysPteIndex 的索引(即下标值)。当需要从系统 PTE 区域中分配给定数量的页面时,比如NumberOfPtes(≤16),使用NumberOfPtes作为MmSysPteTables的下标就能直接得到最小的且大于等于NumberOfPtes的MmSysPteIndex元素的下标。因此,MmSysPteTables提供了快速查表就能得到这 5 种大小之中最合适的下标值的能力。

数组MiSystemPteNBHead 是一个重要的数据结构,它为每一种大小的连续页面内存块定义了一个队列。当客户需要申请一个内存块,即一段连续页面的地址范围时,内存管理器直接从对应的队列中取一块交给客户,若队列中内存块的数量减少了,降到了MmSysPteMinimumFree 数组定义的阈值以下时,系统再往队列中加一些相同大小的内存块。数组MmSysPteListBySizeCount 记录了每种大小的内存块的数量。数组 MmTotalFreeSystemPtes、MmSystemPtesStart、MmSystemPtesEnd 和MmFirstFreeSystemPte分别记录了系统PTE 区域(和非换页内存池扩展区域)中空闲PTE 的数量、起始PTE 、结束PTE 和第一个空闲PTE 。MmFirstFreeSystemPte的元素相当于是一个链表头,指向第一个空闲簇,系统PTE区域中所有的空闲簇直接在PTE 中连接起来,形成一个PTE 链表,如前所述。

当客户请求释放内存块时,如果对应大小的队列中尚有空间可供插入,则直接插入到队列中;如果已经达到设定的最大限制或插入队列失败,则归还到PTE 维护的链表中,必要时可以合并成更大的簇。

下面我们讨论实现系统PTE 区域内存管理算法的代码,位于base\ntos\mm\sysptes.c文件中。主要的函数有三个:MiInitializeSystemPtes、MiReserveSystemPtes 和MiRelease- SystemPtes 。

在系统的阶段0 初始化过程中,当MiInitMachineDependent函数完成了系统非换页内存池的初始化以后,它调用MiInitializeSystemPtes函数来执行系统PTE 区域的初始化,见base\ntos\mm\i386\init386.c 文件的3 295 行。MiInitializeSystemPtes函数的代码位于sysptes.c 的2 247~2 510行。它首先设定系统 PTE 区域的起始 PTE 和结束 PTE 的地址,即MmSystemPtesStart和MmSystemPtesEnd 中对应于SystemPteSpace 数组项的元素,然后将这段PTE 全部清零。

在MiInitializeSystemPtes函数中,它也初始化一条单链表,其中每个节点是一块内存,称为chunk 。MmSysPteIndex 数组实际上定义了5 种chunk 的大小:1,2 ,4,8 和16。在MiInitializeSystemPtes函数中有一个局部数组Lists ,定义了每种chunk 的数量,在Intel x86平台上分别为:400,200,60,50和40。这样累加起来,共涉及PTE 数量为2 080 个,chunk 总数为750 个。这些 chunk 节点本身是从系统非换页内存池中分配的,它们加入到由全局变量MiSystemPteSListHead 定义的单链表中。然后利用此单链表来初始化队列数组MiSystemPteNBHead ,因此,由MiSystemPteSListHead 定义的单链表对于外部不再可见,以后由MiSystemPteNBHead 数组中的队列自动取用单链表中的节点。关于这部分代码逻辑,请参考sysptes.c 文件的 2 359~2 427行。

接下来通过以下一段代码完成对这些队列的初始化,它首先保留一个大的内存块,然后逐个释放各种小尺寸的内存块。这段代码的思路是,利用MiReserveSystemPtes申请一个足够大的空闲PTE 簇(包含TotalPtes 个页面),然后按小块来释放这些页面,MiReleaseSystemPtes 函数自动将这些小内存块加入到各种大小的队列中。
        PointerPte = MiReserveSystemPtes (TotalPtes, SystemPteSpace);
 
        if (PointerPte == NULL) {
            MiIssueNoPtesBugcheck (TotalPtes, SystemPteSpace);
        }
 
        i = MM_SYS_PTE_TABLES_MAX;
        do {
            i -= 1;
            do {
                Lists[i] -= 1;
                MiReleaseSystemPtes (PointerPte,
                                     MmSysPteIndex[i],
                                     SystemPteSpace);
                PointerPte += MmSysPteIndex[i];
            } while (Lists[i] != 0);
        } while (i != 0);

最后,MiInitializeSystemPtes函数完成初始化工作。

现在我们来看MiReserveSystemPtes 是如何分配非换页页面的。它只有两个参数:NumberOfPtes 指页面的数量;SystemPtePoolType 只有两种可能:SystemPteSpace 或NonPagedPoolExpansion,我们只考虑 SystemPteSpace 的情形。首先根据 NumberOfPtes参数的值确定应该使用哪种大小的队列,如果能从队列中提取出一个节点,则直接把队列节点中的内存块(即起始 PTE )解出来返回给客户。而且,如果此种大小的内存块数量小于预定的最小数,即MmSysPteListBySizeCount数组的对应元素小于MmSysPteMinimumFree数组相应的元素,则调用MiFeedSysPtePool 函数,以获得更多的此种大小的内存块。MiFeedSysPtePool 函数的逻辑很简单,其代码位于 base\ntos\mm\sysptes.c 文件的475~538行,它循环10次,每次调用MiReserveAlignedSystemPtes函数获得指定大小的 PTE 内存块,然后调用MiReleaseSystemPtes 函数自动插入到相应的队列中。其结果相当于往特定的队列中插入10个指定大小的PTE 内存块。

回到MiReserveSystemPtes函数中,如果参数NumberOfPtes指定的页面数量超过了16,或者从队列中无法获得指定大小的内存块,则调用 MiReserveAlignedSystemPtes函数申请新的内存块,并返回。

所以,我们现在来看 MiReserveAlignedSystemPtes 函数的逻辑。该函数比MiReserveSystemPtes 多一个对齐参数。MiReserveSystemPtes调用此函数时,设定对齐参数为0,因此我们这里不考虑对齐的情形。MiReserveAlignedSystemPtes首先从第一个空闲PTE 开始,即 MmFirstFreeSystemPte数组的元素,在一个 while循环中找到合适大小的空闲PTE 簇,见代码672~781 行。这是一个顺序扫描过程,如果找到第一个满足条件的簇恰好是所要求的大小,则执行ExactFit 分支,把该簇从空闲簇链表中摘除下来;否则,找到的簇大于所要求的页面数,则把原来的簇缩小,后面的部分作为结果页面。如果对齐参数超过一个页面,则扫描逻辑更加复杂一些,但本质上仍然是找到满足要求的 PTE 簇。
 
最后,刷新这些页面PTE 的硬件快表(TLB )项。然后返回。

理解了申请PTE 的过程以后,我们现在来看释放PTE 的过程,见MiReleaseSystemPtes函数,其代码位于 sysptes.c 文件的1 822~2 129行。该函数的参数指定了要释放的 PTE 起始位置和个数,以及PTE 区域的类型。同样地,我们只考虑 SystemPteSpace 的情形,即系统PTE 区域中的PTE 释放过程。在调用此函数以前,指定范围的PTE 必须是无效的,也就是说,对应的虚拟地址没有被映射到物理页面。所以,MiReleaseSystemPtes 函数首先把这些PTE 全部清零。如果待释放的PTE 数量小于等于16,则考虑把这段PTE 范围插入到MiSystemPteNBHead 数组所指的队列中,而不是直接回收到空闲 PTE 链表中。参见代码1 901~1 974行。

如果插入队列不成功,或者 PTE 数量超过了16,则需要归还到 PTE 链表中。首先从第一个空闲PTE 开始,即 MmFirstFreeSystemPte数组的元素,在一个 while循环中找到第一个其NextEntry域超过当前起始 PTE 的空闲簇,见代码2 004~2 128行。这是一个顺序扫描过程,因为PTE 链表是按照地址先后顺序排列的。找到这样的空闲簇以后,首先考虑是否能跟它合并成一个更大的空闲簇,如果此空闲簇的紧后面恰好是要释放的PTE 范围,则将两者合并起来,见代码2 030~2 048行;如果不能合并,则插入一个新的空闲簇在它后面,见代码2 060~2 080行。接着,如果要释放的PTE 范围恰好紧贴下一个空闲簇,则将两者合并,见代码2 088~2 111行。然后函数结束。

最后简单小结一下,系统 PTE 区域是指把PTE 当做资源来管理,这些 PTE 按照顺序构成了一个单链表。当内核代码通过MiReserveSystemPtes申请得到一段连续的PTE 时,它必须自己映射物理页面。MiReserveSystemPtes 函数仅仅返回了这段PTE 的起始地址,它并不负责填充PTE 中的内容。对称地,当内核代码调用 MiReleaseSystemPtes 函数来释放PTE 范围时,它必须先解除这些PTE 的页面映射。这两个函数既可以用于对系统PTE区域的内存管理,也用于非换页内存池扩展区域的内存管理,关键的区别在于:系统PTE区域的管理算法采用了5 个队列作为缓存,以缓解频繁的保留和释放操作所带来的开销;而非换页内存池扩展区域未引入队列缓存,所以它的保留和释放总是直接扫描 PTE 链表。在Windows 系统中,系统PTE 区域主要用于I/O、内核栈等动态映射页面的情形,而非换页内存池扩展区域仅用于非换页内存池的扩展。

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

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