5.4.4 页框管理对象(PageFrameManager)
页框管理对象用于对物理内存的分页区域(0x01400000到物理内存的末端)进行管理。为了管理方便,对物理内存进行分页管理,每页的大小为PAGE_FRAME_SIZE,可以根据不同的CPU类型确定定义为4KB或8KB等。为了管理每个页框,使用一个页框对象来描述,代码如下定义:
BEGIN_DEFINE_OBJECT(__PAGE_FRAME) __PAGE_FRAME* lpNextFrame; __PAGE_FRAME* lpPrevFrame; DWORD dwKernelThreadNum; DWORD dwFrameFlag; __COMMON_OBJECT* lpOwner; __PRIORITY_QUEUE* lpWaitingQueue; __ATOMIC_T Reference; LPVOID lpVirtualAddr; END_DEFINE_OBJECT()
其中,lpVirtualAddr用来描述该页框被映射到的虚拟内存地址(虚拟地址),如果页框没有被映射,则该虚拟地址不做任何设置。
把物理内存分成一个一个的页框,每个页框的大小为PAGE_FRAME_SIZE(当前版本中该数字定义为4KB),对每一个页框分配一个页框对象,系统中所有页框的页框对象结构组合在一起形成一个数组,整体结构请参考图5-19。
图5-19 Hello China的页框管理
需要注意的是,页框对象数组是动态申请的,即通过KMemAlloc(以KMEM_SIZE_TYPE_ANY为参数)从核心内存池中分配。这是因为不同的硬件配置,物理内存的数量也不一样,所以无法事先确定页框数组的大小。操作系统初始化时,根据检测到的物理内存的数量,计算出页框数组所需要的尺寸,然后动态申请。申请页框数组内存完成之后,操作系统根据内存情况初始化页框管理数组,并且记录下物理内存的起始地址,这样就建立了页框管理数组和物理内存之间的一一对应关系。因此,给定一个页框对象的索引就可以唯一地确定一块物理内存(尺寸为PAGE_FRAME_SIZE),相反,给定任何一个物理地址,就可以确定该物理地址对应的页框对象。比如给定一个页框索引为N,那么相应的物理内存块初始地址可以这样计算:
lpPageFrameAddr=lpStartAddr+PAGE_FRAME_SIZE*N;
相反,给定一个物理地址,假设为lpPageFrameAddr,那么对应的页框对象在页框数组内的索引可以这样确定:
dwIndex=(lpPageFrameAddr-lpStartAddr) / PAGE_FRAME_SIZE;
其中,lpStartAddr为物理内存的起始地址(物理地址)。
实际上,对内存的请求往往不是一个页框,而是许多页框组成的块,因此,为了更有效地利用内存,我们采用伙伴算法(buddy system algorithm)来对物理页框进行进一步的管理。伙伴算法的一个核心思想就是,通过尽量地合并小的块来形成大的块,以避免内存浪费。有关伙伴算法的具体流程,请参考与数据结构相关的书籍。
在伙伴算法中,把数量不同的连续的物理页框组合成块,进行分配时,根据请求的大小选择合适的块分配给请求线程。在当前的实现中,一个线程可以请求下列尺寸大小的内存块:
● 4KB;
● 8KB;
● 16KB;
● 32KB;
● 64KB;
● 128KB;
● 256KB;
● 512KB;
● 1024KB;
● 2048KB;
● 4096KB;
● 8192KB。
可以看出,申请的内存块的尺寸是4KB的二次方倍数。
在系统核心数据区维护了下面一个数据结构(参考图5-20)。
图5-20 Hello China的物理页框管理结构
有一个单独的管理对象——页框管理器(PageFrameManager)管理所有的页框和页块,当一个线程请求一块页块时,页框管理器进行下列动作:
① 判断申请的块是否超出了最大范围(MAX_CHUNK_SIZE,当前版本定义为8MB),如果是,则返回一个空指针;
② 如果没有超出,则从第一个页块控制数组(FrameBlockArray)开始搜索,判断哪个尺寸的页块适合请求者的要求,比如请求者请求了500KB的页块,那么PageFrameManager会认为512KB的页块符合要求;
③ 判断512KB页块的空闲列表是否为空,如果不为空,则从中删除一块,返回请求者对应页块的指针;
④ 如果为空,则依次往下(页块尺寸大的方向)搜索,直到找到一块更大的页块,或者失败(搜索到最后一级,仍然没有空闲块)为止;
⑤ 如果失败,返回用户一个空指针;
⑥ 否则,依次对分找到的更大的页块,并插到上一级页块空闲列表中,直到找到512KB页块为止,然后返回用户经过对分最后剩下的页块的指针。
每个页块空闲列表控制结构都对应一个位图,系统通过这个位图来判断对应块空闲或被使用,这样释放页块时,可以根据位图把连续的页块(伙伴块)组装成更大的页块。
下面是页框管理对象的定义代码:
BEGIN_DEFINE_OBJECT(__PAGE_FRAME_MANAGER) __PAGE_FRAME* lpPageFrameArray; __PAGE_FRAME_BLOCK FrameBlockArray[PAGE_FRAME_BLOCK_NUM]; DWORD dwTotalFrameNum; DWORD dwFreeFrameNum; LPVOID lpStartAddress; BOOL (*Initialize)(__COMMON_OBJECT* lpThis, LPVOID lpStartAddr, LPVOID lpEndAddr); LPVOID (*FrameAlloc)(__COMMON_OBJECT* lpThis, DWORD dwSize, DWORD dwFrameFlag); VOID (*FrameFree)(__COMMON_OBJECT* lpThis, LPVOID lpStartAddr, DWORD dwSize); END_DEFINE_OBJECT()
其中,FrameBlockArray数组用来描述不同大小的页框。按照目前的实现,页框大小按照尺寸组织成4KB、8KB等总共12种,所以该数组的大小(PAGE_FRAME_BLOCK_NUM的大小)为12。lpPageFrameArray是一个页框对象类型的数组,该数组由操作系统启动过程中根据检测到的物理内存的数量动态分配。系统中的物理内存(除去OS核心占用的内存)被分割成以PAGE_FRAME_SIZE为大小的页框块,每块物理内存对应一个页框对象(__PAGE_FRAME),假设系统中物理内存的大小为32MB,其中OS核心占用了20MB,系统中剩下的12MB内存以4KB为单位分割成12MB/4KB=3KB块,因此,lpPageFrameArray数组的大小就是3KB×sizeof(__PAGE_FRAME)。
Initialize函数是该对象的初始化函数,该对象是一个全局对象,即整个系统中只存在一个,因此,其初始化函数在操作系统初始化的过程中被调用。LpStartAddr参数和lpEndAddr参数是系统中用于分页管理的物理内存的起始地址和结束地址,Initialize函数根据这两个参数计算出系统中需要分页管理的物理内存的大小,并根据这两个参数以及计算出来的大小,初始化页框管理对象的相关变量(lpPageFrameArray、FrameBlockArray等)。在目前Hello China的实现中,需要分页管理的物理内存的起始地址定义为0x01400000(20MB以后),结束地址根据检测的物理内存数量设定,比如检测到系统中有64MB的物理内存,则操作系统初始化时,这样调用该函数:
PageFrameManager.Initialize(&PageFrameManager,0x01400000,0x03FFFFFF);
FrameAlloc和FrameFree两个函数分别用于具体的页框申请和页框释放操作。FrameAlloc函数根据调用程序给出的页框需求大小,分配一个或多个连续的页框,返回所分配的页框的初始物理地址,若分配失败(比如系统中没有足够的页框),则返回NULL。FrameFee函数则用于释放PrameAlloc分配的页框,除了指定要释放的页框的首地址外,还需要指定要释放的页框的尺寸(dwSize参数)。
需要注意的是,若启用了CPU提供的分页机制,(FrameAlloc和FrameFree),一般情况下应用程序不要直接调用页框管理对象提供的这两个函数,而应该调用虚拟内存管理对象(下面介绍)提供的VirtualAlloc函数来具体分配内存。因为直接调用这两个函数,不会更新系统中的页表和页目录,会造成系统数据的不一致,而且直接访问FrameAlloc返回的内存地址,可能会引起异常(因为这时候系统页表没有更新)。
若没有启用CPU的分页机制,则页框管理器提供的这两个页面分配函数可以由应用程序调用来完成物理内存的分配。但这两个函数只能完成以4KB大小为粒度的物理内存的分配,但无法满足更小粒度的物理内存的分配。目前版本的Hello China没有实现这种用户应用程序层面的小粒度的内存分配函数,在这种情况下可考虑由应用程序编写者自己编写一个内存分配器,下面是一种可选择的思路:
(1)应用程序初始化时,调用页框管理器提供的FrameAlloc函数分配一定数量的物理内存(比如32KB);
(2)把申请的上述32KB内存作为应用程序内存池,然后使用空闲链表算法,自己设计一个内存分配器(提供malloc和free等标准C库函数);
(3)应用程序每次申请小于4KB的内存时,就调用应用程序开发者自己编写的malloc函数进行分配;
(4)在内存池不足的情况下,内存分配器可以通过再次调用FrameAlloc函数分配更多的内存。
实际上,很多操作系统实现对内存的管理都是以页大小为基础进行的,没有提供更小粒度的内存分配器。更小粒度的内存分配器,在应用程序层面实现。
到此为止,Hello China物理内存的管理机制就介绍完毕了,后续部分将详细介绍虚拟内存(基于IA32提供的分页机制)的实现机制。表5-6对物理内存管理机制的要点,进行了总结。
表5-6 Hello China的内存访问服务接口