5.4.5 页面索引对象(PageIndexManager)
所谓页面索引对象,指的是用于完成虚拟地址和物理地址转换功能的数据结构,比如页目录、页表等。这种数据结构因不同的硬件平台(CPU)而不同,比如针对Intel IA32系列的CPU,页目录和页表构成了页索引对象,而对于像PowerPC等RISC结构的CPU,则采用HASH算法,根据虚拟地址完成物理地址的计算,这样又有了另外一套索引对象(索引数据结构)。在Hello China的实现中,把所有这些功能使用同一个对象——页面索引管理器来进行封装。
PageIndexMgr用来管理页索引,这个对象的功能以及内部实现是与具体的处理器平台密切关联的,比如针对Intel的IA32构架,该对象完成该平台下的页目录、页表以及页目录项和页表项的管理,该对象定义代码如下:
BEGIN_DEFINE_OBJECT(__PAGE_INDEX_MANAGER) INHERIT_FROM_COMMON_OBJECT __PDE* dwPdAddress; BOOL (*Initialize)(__COMMON_OBJECT*); VOID (*Uninitialize)(__COMMON_OBJECT*); LPVOID (*GetPhysicalAddress)(__COMMON_OBJECT*,LPVOID); BOOL (*ReservePage)(__COMMON_OBJECT*, LPVOID,LPVOID,DWORD); BOOL (*SetPageFlag)(__COMMON_OBJECT*, LPVOID, LPVOID, DWORD); VOID (*ReleasePage)(__COMMON_OBJECT*,LPVOID); /*__PTE* (*GetPte)(__COMMON_OBJECT*,LPVOID); __PDE* (*GetPde)(__COMMON_OBJECT*,LPVOID); VOID (*SetPteFlags)(__COMMON_OBJECT*,__PTE*,DWORD); VOID (*SetPdeFlags)(__COMMON_OBJECT*,__PDE*,DWORD);*/ END_DEFINE_OBJECT()
其中,dwPdAddress是页目录的物理地址,在Intel构架的CPU中加载到CR3寄存器中,用来定位页目录。下面对该对象提供的主要接口函数进行讲解。
1.Initialize
该函数是页索引管理对象的初始化函数,在当前版本的实现中,该函数做如下工作:
(1)初始化页目录,并把内核占用的头20MB空间所占用的页目录项和页表项填满。在当前的实现中,整个系统只有一个页目录(没有实现不同地址空间的进程),把页目录固定地放置在物理内存PD_START开始的内存处(PD_START定义为0x00200000-0x00010000),占用4KB的物理地址空间,然后把20MB物理地址空间所占用的页表(共5个页表框,20KB内存)项紧接着页目录存放,并进行填充。如图5-21所示。
图5-21 Hello China的核心页目录和页表在内存中的位置
因为前20MB是操作系统和设备驱动程序代码、数据占用的空间,所以需要事先填写好。需要注意的是,对于页目录中没有占用的目录项,都使用EMPTY_PDE_ENTRY填充,即都填充成空。前20MB物理内存跟线性地址空间的映射关系采用的是“照实映射”,即映射到线性地址空间内的相同的位置(线性地址空间的前20MB处)。这样做的好处是分页机制可以透明地显示给操作系统核心代码,在编写、编译操作系统核心代码时,无须考虑分页机制。
(2)完成页目录和页表的填充后,设置dwPdAddress为PD_START并加载到CR3寄存器中(这时系统仍然工作在非分页模式,直到所有初始化任务结束后,系统才设置CR0寄存器,使得整个系统转到分页模式)。
需要注意的是,在目前的实现中,系统中只有一个PageIndexManager对象(因为没有实现独立虚拟内存空间的进程模型,整个系统只有一个虚拟内存空间,各核心线程共享这个虚拟内存空间),而这个虚拟内存空间的页索引数据结构(页表、页目录等)被固定在了PD_START的位置(物理内存),因此不用调用KMemAlloc函数额外分配页目录和页表。但如果是对多进程模型(在多进程模型下每个进程需要有一个页索引管理器),则该函数应该调用KMemAlloc函数为新创建的进程分配页索引对象(页表、页目录等),并初始化这些页索引对象(包括把内核的前20MB内存空间映射到新创建进程的虚拟地址空间中,以使得这前20MB地址空间可以被任何进程访问)。
这样一个问题就出现了,即页索引管理对象如何判断自己是在操作系统初始化过程中调用(这是第一个页索引对象),还是在操作系统运行过程中创建进程时调用。这个问题可以通过下列办法解决:
在第一次调用时,页索引管理器对象需要完成FD_START位置的页目录的初始化,这样该位置的页目录项将会是一个合法的目录项,后续相关调用可以检查该位置是否是一个合法的目录项,或者是一个空目录项(系统初始化时填充成空目录项),如果是空,则是第一次调用,直接初始化即可,否则,需要调用KMemAlloc函数分配页索引对象空间。
2.Uninitialize
与Initialize对应,该函数判断自己是针对进程调用,还是针对系统中的唯一页索引管理器对象调用。如果是后者,不需要做任何事情,如果是前者,则需要释放所有由页索引对象占用的地址空间。
由于lpPdAddress已经包含了页目录的物理地址,因此,只要对比该地址是否是PD_START就可以判断是不是针对进程调用。
3.GetPhysicalAddress
该函数完成虚拟地址到物理地址的转换。该函数根据CPU特定的转换机制,通过适当地分割虚拟地址,然后查找页索引而得到物理地址。在Intel的IA32构架的CPU上,该函数这样处理:首先,把虚拟地址的开始10bit作为页目录的索引,找到一个页目录项,从页目录项中得到页表的物理地址;然后利用虚拟地址的中间10bit作为页表的索引,找到一个页表项,通过页表项找到页框的物理地址;最后以虚拟地址的最后12bit为偏移,加上页框的物理地址就可以得到该虚拟地址对应的物理地址。上述能够操作的前提是该虚拟地址对应的页框存在于物理内存中。如果不存在,或者存在但被调换出去(后续版本实现),则返回一个NULL值。
4.ReservePage
该函数为虚拟地址分配一个页表项,原型如下:
BOOL ReservePage(__COMMON_OBJECT* lpThis,LPVOID lpVirtualAddr,LPVOID lpPhysicalAddr,DWORD dwFlags);
其中,lpVirtualAddr是虚拟地址,而lpPhysicalAddr则是物理地址,dwFlags是页表项的属性。该函数的任务就是通过在页索引对象中设置合适的页表和页目录项,完成lpVirtualAddr和lpPhysicalAddr的映射。
dwFlags可以取下列值:
#define PTE_FLAG_PRESENT 0x001 #define PTE_FLAG_RW 0x002 #define PTE_FLAG_USER 0x004 #define PTE_FLAG_PWT 0x008 #define PTE_FLAG_PCD 0x010 #define PTE_FLAG_ACCESSED 0x020 #define PTE_FLAG_DIRTY 0x040 #define PTE_FLAG_PAT 0x080 #define PTE_FLAG_GLOBAL 0x100 #define PTE_FLAG_USER1 0x200 #define PTE_FLAG_USER2 0x400 #define PTE_FLAG_USER3 0x800
如果dwFlags包含了PTE_FLAG_PRESENT位,但lpPhysicalAddr为NULL,则认为是一个错误,直接返回FALSE。否则,该函数完成下列操作:
① 根据页目录索引(虚拟地址的前10bit)找到对应的页目录项,判断该页目录项是否存在(或者是否已经使用,通过调用EMPTY_PDE_ENTRY宏来判断),如果没有使用则说明该页目录对应的页表也不存在,于是先调用KMemAlloc分配一个页表(4KB),对该内存进行清0,并根据页表的物理地址初始化页目录项;
② 根据页目录项找到对应的页表,根据页表索引(虚拟地址的中间10bit)找到对应的页表项,判断该页表项是否存在(通过调用EMPTY_PTE_ENTRY宏判断),如果存在则直接返回TRUE;
③ 如果页表项不存在,则预留该页表项,并根据dwFlags的值设置该页表项的标记(FLAG),并进一步判断dwFlags是否包含PTE_FLAG_PRESENT位,如果包含则使用lpPhysicalAddr设置页表项的页框物理地址;
④ 上述所有步骤完成之后,返回TRUE。
需要格外说明的是,该函数参数的两个地址(lpVirtualAddr和lpPhysicalAddr)都需要是4KB边界对齐的,否则函数会直接返回FALSE。这个条件需要调用者保证(即在调用该函数前,首先确保上述两个地址满足4KB边界对齐的要求)。
5.SetPageFlag
该函数用于设置页表项的标记属性,原型如下:
BOOL SetPageFlag(__COMMON_OBJECT* lpThis,LPVOID lpVirtualAddr,LPVOID lpPhysicalAddr,DWORD dwFlags);
其中lpVirtualAddr是虚拟地址,用于设置由该虚拟地址对应的页表项属性。需要注意的是,该虚拟地址一定是4KB边界(页长度边界)对齐的,否则该函数将直接返回FALSE。dwFlags是希望设置的属性标志,如果dwFlags包含了PTE_FLAG_PRESENT标志位,则lpPhysicalAddr一定不能为NULL,该数值包含了该页表项对应的页框物理地址,也是4KB边界对齐的。
该函数进行如下操作:
① 判断该虚拟地址对应的页目录项和页表项是否存在,任何一个不存在,都将导致该函数直接返回FALSE;
② 找到对应的页表项后,首先检查原页表项的标记是否与dwFlags一致,如果一致,直接返回TRUE;
③ 使用dwFlags的值代替原页表的标记属性,如果dwFlags设置了PTE_FLAG_PRESENT位,则再使用lpPhysicalAddr设置页表项对应的页框的物理地址;
④ 上述所有操作完成之后,返回TRUE。
6.ReleasePage
该函数释放虚拟地址占用的页表项,原型如下:
VOID ReleasePage(__COMMON_OBJECT* lpThis,LPVOID lpVirtualAddr);
其中,lpVirtualAddr就是要释放的页表项所对应的虚拟地址。该函数首先检查对应的页表项是否存在,如果不存在,直接返回,否则,把对应的页表项设置成一个空页表项,然后检查该页表项对应的页表框是否还有保留的页表项(通过调用EMPTY_PTE_ENTRY判断)。如果该页表框已经没有保留的页表项了,则释放该页表框占用的内存,并设置对应的页目录项为空,然后返回。
之所以在该函数中检查并释放页表框,是为了与ReservePage对应,确保系统中不存在内存浪费。
7.页索引管理器的应用
页索引管理器对特定CPU的分页机制进行了封装,使得不同的分页机制对外表象出相同的处理接口,这样便于代码的移植。需要注意的是,页索引管理器仅仅完成页索引的操作,比如预留、释放、设置标志等,一般情况下,应用程序不要直接调用这些操作接口,以免引起系统的崩溃。该对象提供的接口函数,被虚拟内存管理器调用来完成线性地址空间内的页面与物理地址空间内的页面之间的映射。