5.4.6 虚拟内存管理对象(VirtualMemoryManager)
1.虚拟区域
采用虚拟区域(Virtual Area)来表示线性内存空间中的一个区域。需要注意的是,这个区域仅仅是线性内存空间的一部分,不一定与物理内存存在映射关系,这个时候,如果引用该虚拟内存空间,由于没有与物理内存对应,所以会导致访问异常。因此,在使用虚拟内存区域前,一定要通过API调用来完成虚拟内存到物理内存的映射。
但虚拟内存区域不仅仅可以与物理内存之间完成映射,甚至可以与存储系统上的文件、硬件设备的内存映射区域等映射。比如可以把一个存储系统文件的部分内容(或全部内容)映射到一个进程(或系统)的虚拟地址空间中,这个时候,只要按通常的内存访问方式就可以访问文件中的内容了,十分方便。内存和文件之间的同步,由操作系统保证,对应用程序来说是透明的。
还有一个应用就是把设备的内存映射区域映射到虚拟空间中,这时候只要访问虚拟内存中的相关区域,就可以直接访问设备了。
在当前版本的实现中,Hello China只实现了虚拟内存的基本功能,即可以把虚拟地址空间中的一个区域(由虚拟区域描述),映射到物理内存或设备的IO内存映射区域中,通过对虚拟内存的访问来完成对设备或物理内存的访问。
针对每块虚拟区域有一个虚拟区域描述符进行描述、管理,虚拟区域描述符(Virtual Area Descriptor)的定义如下:
DECLARE_PREDEFINED_OBJECT(__VIRTUAL_MEMORY_MANAGER); BEGIN_DEFINE_OBJECT(__FILE_OPERATIONS) DWORD (*FileRead)(__VIRTUAL_MEMORY_DESCRIPTOR*); DWORD (*FileWrite)(__VIRTUAL_MEMORY_DESCRIPTOR*); END_DEFINE_OBJECT() //This object is not used currently ,but maybe //used in the future. BEGIN_DEFINE_OBJECT(__VIRTUAL_AREA_DESCRIPTOR) __VIRTUAL_MEMORY_MANAGER* lpManager; LPVOID lpStartAddr; LPVOID lpEndAddr; __VIRTUAL_AREA_DESCRIPTOR* lpNext; DWORD dwAccessFlags; DWORD dwCacheFlags; DWORD dwAllocFlags; __ATOMIC_T Reference; DWORD dwTreeHeight; __VIRTUAL_AREA_DESCRIPTOR* lpLeft; __VIRTUAL_AREA_DESCRIPTOR* lpRight; UCHAR strName[MAX_VA_NAME_LEN]; __FILE* lpMappedFile; DWORD dwOffset; __FILE_OPERATIONS* lpOperations; END_DEFINE_OBJECT()
在当前的实现中,把描述每块虚拟区域的虚拟区域描述符通过链表的方式连接在一起(lpNext指针),形成图5-22所示的结构。
图5-22 Hello China的虚拟区域管理结构
之所以对虚拟区域(线性地址空间)进行统一管理,是因为线性地址空间也是一种重要的系统资源,许多实体,比如设备驱动程序等,都需要一块线性地址空间来完成设备寄存器的映射,这时候若不进行统一管理、统一分配,就可能会导致冲突,即两个实体占用了同一块线性地址空间区域。进行统一管理后,操作系统提供统一的接口给应用程序或驱动程序,用以申请线性地址空间的某一块区域。在受理虚拟区域申请的时候,操作系统首先检索整个线性地址空间的分配情况(通过遍历虚拟区域描述符链表来实现),从未分配的区域中选择一块合适的分配给应用程序或设备驱动程序,并设置该虚拟区域对应的页表和页目录(通过页索引对象来实现)。在32位线性地址空间中,整个线性地址空间的大小为4GB,这是一个庞大的空间,如果有大量的虚拟区域描述符存在,则遍历虚拟区域描述符链表将是一件非常耗时间的事情,因为遍历链表花费的时间,跟链表元素的数量是成正比例关系的。这种情况下,为了提高系统的效率,Hello China采用了两种数据结构管理虚拟区域描述符,除了上述的链表方式外,还采用了平衡二叉树的方式。在目前的实现中,若虚拟区域描述符的数量小于64,则采用链表进行管理,若一旦虚拟区域描述符的数量超过了这个数值,则切换到平衡二叉树进行管理。
上述虚拟区域组成的链表(或二叉树)是由虚拟内存管理器(Virtual Memory Manager)进行管理的。每个具有独立地址空间的进程都有一个对应的虚拟内存管理器对象,当前的实现中,由于没有引入进程的概念,所以只有一个全局的虚拟内存管理器来管理整个系统的虚拟内存,所有的内核线程对象都共享这个虚拟内存管理器,进而共享整个的虚拟内存空间。
lpStartAddr和lpEndAddr分别指明了本虚拟区域的起始虚拟地址和结束虚拟地址,而dwAccessFlags和dwCacheFlags则分别指明了本虚拟区域的访问属性和缓存属性。访问属性可以取下列值:
#define VIRTUAL_AREA_ACCESS_READ 0x00000001 #define VIRTUAL_AREA_ACCESS_WRITE 0x00000002 #define VIRTUAL_AREA_ACCESS_RW 0x00000004 #define VIRTUAL_AREA_ACCESS_EXEC 0x00000008
上述各值在请求该虚拟区域时指定(一般由调用者指定)。
缓存属性可以取下列值:
#define VIRTUAL_AREA_CACHE_NORMAL 0x00000001 #define VIRTUAL_AREA_CACHE_IO 0x00000002 #define VIRTUAL_AREA_CACHE_VIDEO 0x00000004
其中,VIRTUAL_AREA_CACHE_NORMAL指明了当前虚拟区域如果跟物理内存进行关联,则使用缺省的内存缓冲策略(也就是物理内存跟L1、L2和L3等处理器Cache之间的缓冲/替换策略),一般情况下,缺省的缓存策略为回写方式,即对于读操作直接从CACHE里面读取,如果没有命中,则引发一个CACHE行更新;对于写操作,写入CACHE的同时,直接写入物理内存,即写操作所影响的数据不会在CACHE中缓存,而是直接反映到物理内存中。但需要注意的是,对于写操作,处理器可能会使用内部的写合并(Write Combine)缓冲区。
VIRTUAL_AREA_CHCHE_IO则指明了当前的虚拟区域是一个IO设备的映射区域,这样对该区域的CACHE策略是禁用系统CACHE,并禁用投机读等提高效率的策略,而应该严格按照软件编程顺序对虚拟区域进行访问。这是因为设备映射的IO区域,一般情况下是跟物理设备的寄存器对应的,而这些物理设备的寄存器可能处于不断的变化当中,若采用Cache缓存的读策略,则可能出现数据不一致的情况,所以在物理设备驱动程序的实现中,若需要申请虚拟区域,一定要采用VIRTUAL_AREA_CACHE_IO来作为申请标志。一般情况下,对于PCI设备的内存映射区域应该设置这种缓冲策略。
Hello China由于定位于嵌入式的操作系统,即使运行在PC上,也是常驻内存的,不会发生物理内存和存储设备之间的内存替换,而且也没有必要引入进程概念,所以,没有必要实现分页机制。而且按照通常的说法,实现分页机制会导致系统的整体效率大大下降(如果实现了分页机制,对于一次内存的访问,可能需要多次实际的内存读写才能完成,因为CPU要根据页表和页目录来完成实际物理内存的定位,尽管采用TLB等缓冲策略可以提高访问效率,但相对不分页来说,系统效率还是会大大降低),但后来的一些设备,比如网卡、显示卡等物理设备,需要把内部寄存器映射到存储空间,而且这些存储空间还不能采用缺省的内存缓冲策略。这样就必须采用一些额外机制,保证这些内存映射区域的完整性(不会因为提前读而导致数据不一致),而分页是一种最通用的内存控制策略,可以在页级对虚拟内存区域属性进行控制,因此,在目前的Hello China版本中,实现了基于分页机制的虚拟内存管理系统,而且这个系统是可裁剪的,即通过调整适当的编译选项,可以选择编译后的内核是否包含该系统。
后续Hello China的实现可能会因为额外的需要,实现一个更完整的虚拟内存系统(比如增加文件和虚拟内存的映射、页面换出等功能),但至少目前还没有这个必要。
另外,在Intel的处理器上可以通过设置一些控制寄存器,比如MTTR等,来控制缓存策略,但不作为一种通用的方式,在当前Hello China的实现中也不作考虑。
VIRTUAL_AREA_CACHE_VIDEO是另外一种CACHE策略,这种策略可以针对VIDEO的特点进行额外优化,在这里不做详细描述。
为了将来扩充方便,在当前虚拟区域的定义中也引入了相关变量,来描述虚拟区域和存储系统文件之间的映射关系,但在当前版本中没有实现该功能,其一是因为没有必要(就目前Hello China的应用来说),其二是因为Hello China没有实现文件系统(将来的版本中会实现)。
最后要说明的是,为便于描述每个虚拟区域,在虚拟区域描述对象中引入了虚拟区域名字变量,其最大长度是MAX_VA_NAME_LEN(目前定义为32),该变量在分配虚拟区域的时候,被虚拟内存管理器填写,当然,最初的来源仍然是由用户指定(参考VirtualAlloc的定义)。
2.虚拟内存管理器(Virtual Memory Manager)
虚拟内存管理器是Hello China的虚拟内存管理机制的核心对象,提供应用程序(或设备驱动程序)可以直接调用的接口完成虚拟内存(线性地址空间)的分配。另外,该对象还维护了虚拟区域描述符链表(或二叉树)等数据。当前版本没有实现进程模型,因此整个系统中只有一个虚拟内存管理器用以对虚拟地址空间进行管理,若实现了进程模型,则每个进程需要有自己的虚拟内存管理器对象。
下面就是虚拟内存管理器对象的定义代码:
BEGIN_DEFINE_OBJECT(__VIRTUAL_MEMORY_MANAGER) INHERIT_FROM_COMMON_OBJECT __PAGE_INDEX_OBJECT* lpPageIndexMgr; __VIRTUAL_AREA_DESCRIPTOR* lpListHdr; __VIRTUAL_AREA_DESCRIPTOR* lpTreeRoot; __ATOMIC_T Reference; DWORD dwVirtualAreaNum; __LOCK_T SpinLock; BOOL (*Initialize)(__COMMON_OBJECT*); VOID (*Uninitialize)(__COMMON_OBJECT*); LPVOID (*VirtualAlloc)(__COMMON_OBJECT*, LPVOID, //Desired start virtual address DWORD, //Size DWORD, //Allocation flags DWORD, //Access flags. UCHAR*, //Virtual area name. LPVOID); //Reserved. VOID (*VirtualFree)(__COMMON_OBJECT*, LPVOID); DWORD (*GetPdeAddress)(__COMMON_OBJECT*); END_DEFINE_OBJECT()
其中,lpPageIndexMgr指向一个页面索引管理对象(__PAGE_INDEX_MANAGER)用来管理该虚拟内存空间的页索引对象(页表、页目录等)。在系统核心中,页面管理对象(PageFrameManager)、页索引管理对象(PageIndexManager)和虚拟内存管理对象(VirtualMemoryManager)的数量关系是,整个系统一个页面管理对象用来管理整个系统的物理内存(因为在单处理系统或对称多处理器系统中,整个系统只有一个共享的物理内存池,不考虑多处理器、多内存池的情况),而一个线性地址空间,对应一个虚拟内存管理对象和一个页面索引管理对象。在当前的实现中,由于整个系统只有一个虚拟内存空间(没有引入进程的概念),所以,整个系统中这三种对象都只有一个。将来如果引入了进程的概念,每个进程一个虚拟内存空间,那么系统中就会存在多个虚拟内存管理对象和多个页面管理对象,但仍然只有一个页面管理对象。这三种类型的内存管理对象的关系如图5-23所示。
图5-23 页框管理器、虚拟内存管理器和页索引管理器
由于当前没有实现多线程模型,所以系统中只有一个操作系统内存空间,图中实线矩形表示当前已经实现的操作系统内存空间,而虚线矩形则表示了每个进程的虚拟地址空间。
lpListHdr指向虚拟区域链表,lpTreeRoot也是用来维护虚拟区域的,在当前的实现中,如果虚拟区域的数量少于MAX_VIRTUAL_AREA_NUM个(当前定义为64),则使用线性表进行管理,如果超出了MAX_VIRTUAL_ZREA_NUM个,则使用平衡二叉树进行管理,以加快查找等操作的速度。
SpinLock用在SMP(对称多处理系统)上,以同步对虚拟内存管理器对象的访问(在单处理器系统上没有任何用途),dwVirtualAreaNum是目前已经分配的虚拟区域的数量。
下面介绍虚拟内存管理器提供的函数,这些仅仅是对外可用的,还有一些函数,作为内部辅助函数没有对外提供,在这里不作介绍。这些函数中,最重要的一个就是VirtualAlloc函数,这个函数是虚拟内存管理系统对外的最主要接口,也是用户线程(或实体)请求虚拟内存服务的唯一接口。
3.Initialize
这是该对象的初始化函数,目前来说,该函数完成下列功能:
(1)设置该对象的函数指针值;
(2)创建第一块虚拟区域(Virtual Area,通过调用KMemAlloc函数实现),起始地址为0,终止地址为0x013FFFFF,长度为20MB(该内存区域被操作系统核心数据和代码、核心内存池等占用),访问属性为VIRTUAL_AREA_ACCESS_RW,缓冲策略为VIRTUAL_AREA_CACHE_NORMAL,并把该虚拟区域对象插入虚拟区域列表;
(3)调用ObjectManager的CreateObject方法创建一个PageIndexManager对象;
(4)调用PageIndexManager的初始化函数(该函数完成系统空间的页表预留工作);
(5)设置dwVirtualAreaNum为1;
(6)如果上述一切正常,返回TRUE,否则返回FALSE。
操作系统初始化时,调用该函数(Initialize),如果该函数失败(返回FALSE),将直接导致操作系统初始化不成功。
4.Uninitialize
目前情况下,该函数不作任何工作,因为该函数只能在操作系统关闭的时候调用(系统中只有一个虚拟内存管理器对象),但是如果在多进程的环境下,该函数调用DestroyObject(ObjectManager对象提供)函数,释放PageIndexManager对象,并删除所有创建的Virtual Area对象。
5.VirtualAlloc
该函数用来分配虚拟内存空间中的内存,是虚拟内存管理器提供给应用程序的最重要接口,该函数原型如下:
LPVOID VirtualAlloc(__COMMON_OBJECT* lpThis, LPVOID lpDesiredAddr, DWORD dwSize, DWORD dwAllocationFlag, DWORD dwAccessFlag, UCHAR* lpVaName, LPVOID lpReserved);
其中,lpDesiredAddr是应用程序的希望地址,即应用程序希望能够得到从lpDesiredAddr开始、dwSize大小的一块虚拟内存,dwAllocationFlag则指出了希望的分配类型,有下列可取值:
#define VIRTUAL_AREA_ALLOCATE_RESERVE 0x00000001 #define VIRTUAL_AREA_ALLOCATE_COMMIT 0x00000002 #define VIRTUAL_AREA_ALLOCATE_IO 0x00000004 #define VIRTUAL_AREA_ALLOCATE_ALL 0x00000008
各标志的含义如下:
● VIRTUAL_AREA_ALLOCATE_RESERVE:该标志指明了应用程序只希望系统能够预留一部分线性内存空间,不需要分配实际的物理内存。VirtualAlloc函数在处理这种类型的请求时,只会检索虚拟区域描述符表,查找一块未分配的虚拟内存区域,并返回给用户,同时,调用PageIndexManager提供的接口建立刚刚分配的虚拟内存对应的页表。需要注意的是,这个时候建立的页表项,其P标志(存在标志)被设置为0,表明该虚拟内存区域尚未分配具体的物理内存,此时,对这一块虚拟内存的访问将会引起异常。
● VIRTUAL_ALLOCATE_COMMIT:使用该标志调用VirtualAlloc的应用程序,希望完成预先预留的(以VIRTUAL_AREA_ALLOCATE_RESERVE调用VirtualAlloc)虚拟内存空间的物理内存分配工作,即为预先分配的虚拟内存预留物理内存空间,并完成页表的更新,此时访问对应的虚拟内存就不会引起异常了。
● VIRTUAL_AREA_ALLOCATE_IO:使用该标志调用VirtualAlloc函数,说明调用者希望预留的虚拟内存区域是用于IO映射。这种情况下,系统不但需要预留虚拟内存空间,而且还要完成系统页索引结构的初始化,即根据预留结果,填写页表。这时候,预留的线性地址空间的地址与采用页索引结构映射到物理地址空间的地址是一样的,直接映射到设备的“寄存器地址空间”。
● VIRTUAL_AREA_ALLOCATE_ALL:采用该标志调用VirtualAlloc函数,说明应用程序既需要预留一部分虚拟内存空间,也需要为对应的虚拟内存空间分配物理内存,并完成两者之间的映射(填写页面索引数据结构)。也就是说,VIRTUAL_AREA_ALLOCATE_ALL是VIRTUAL_AREA_ALLOCATE_RESERVE和VIRTUAL_AREA_ALLOCATE_COMMIT两个标志的结合。
dwAccessFlags说明了调用者希望的访问类型,可以取下列值:
#define VIRTUAL_AREA_ACCESS_READ 0x00000001 #define VIRTUAL_AREA_ACCESS_WRITE 0x00000002 #define VIRTUAL_AREA_ACCESS_RW 0x00000004 #define VIRTUAL_AREA_ACCESS_EXEC 0x00000008
上述各取值的含义都是很明确的。lpVaName指明了虚拟区域的名字,一般用来描述虚拟区域,lpReserved用于将来使用,当前情况下,用户调用时,一定要设置为NULL。
下面根据不同的分配标志,对VirtualAlloc的动作进行详细描述。
VIRTUAL_AREA_ALLOCATE_IO
设置了这个标志,说明分配者希望分配到一块内存映射IO区域,访问IO设备。一般情况下,用户指定了lpDesiredAddr参与是希望系统能够在lpDesiredAddr开始的地方开始分配。
在这种情况下,VirtualAlloc进行如下处理:
① 向下舍入lpDesiredAddr地址到PAGE_FRAME_SIZE边界,在dwSize上增加向下舍入的部分,并向上舍入dwSize到FRAME_PAGE_SIZE边界;
② 检查从lpDesiredAddr开始。长度为dwSize的虚拟内存空间是否已经分配,或者是否与现有的区域重叠,这项检查通过遍历虚拟区域链表或虚拟区域AVL树来完成;
③ 如果所请求的区域既没有分配,也没有与现有区域重叠,则创建一个虚拟区域描述对象(__VIRTUAL_AREA_DESCRIPTOR)设置该对象的相关成员;
④ 如果请求的区域已经分配,或者与现有的区域有重叠,则需要在虚拟地址空间中重新寻找一块区域,如果寻找成功,则创建虚拟区域描述对象,否则,直接返回NULL,指示操作失败;
⑤ 把上述区域描述对象插入链表或AVL树(根据目前虚拟区域数量决定);
⑥ 递增dwVirtuanAreaNum;
⑦ 以FRAME_PAGE_SIZE为递增单位,循环调用lpPageIndexMgr的ReservePage函数,在系统页表中增加对新增加区域的页表项,页表项的虚拟地址和物理地址相同(都是lpDesiredAddr),页表项的属性为PTE_FLAG_PRESENT、PTE_FLAG_NOCACHE,其访问对象根据dwAccessFlags标志设置为PTE_FLAG_READ、PTE_FLAG-WRITE或PTE_FLAG_RW;
⑧设置dwAllocFlags为VIRTUAL_AREA_ALLOCATE_IO,以指明没有为该虚拟内存区域分配物理内存;
⑨ 如果上述一切成功,则返回lpDesiredAddr(注意,该数值可能是最初用户调用的时候设置的数值,也可能是由VirtualAlloc重新分配的数值),以指示调用成功。
下面是上述实现的详细代码:
static LPVOID VirtualAlloc(__COMMON_OBJECT* lpThis, LPVOID lpDesiredAddr, DWORD dwSize, DWORD dwAllocFlags, DWORD dwAccessFlags, UCHAR* lpVaName, LPVOID lpReserved) { switch(dwAllocFlags) { case VIRTUAL_AREA_ALLOCATE_IO: //Call DoIoMap only. return DoIoMap(lpThis, lpDesiredAddr, dwSize, dwAllocFlags, dwAccessFlags, lpVaName, lpReserved); break; case VIRTUAL_AREA_ALLOCATE_RESERVE: … … … default: return NULL; } return NULL; }
VirtualAlloc函数判断分配标志。根据不同的标志再进一步调用特定的实现函数。在分配标志是VIRTUAL_AREA_ALLOCATE_IO的情况下,调用DoIoMap函数,该函数实际完成预留功能,下面是该函数的实现代码。由于函数较长,我们分段解释:
static LPVOID DoIoMap(__COMMON_OBJECT* lpThis, LPVOID lpDesiredAddr, DWORD dwSize, DWORD dwAllocFlags, DWORD dwAccessFlags, UCHAR* lpVaName, LPVOID lpReserved) { __VIRTUAL_AREA_DESCRIPTOR* lpVad =NULL; __VIRTUAL_MEMORY_MANAGER* lpMemMgr =(__VIRTUAL_ MEMORY_MANAGER*)lpThis; LPVOID lpStartAddr=lpDesiredAddr; LPVOID lpEndAddr =NULL; DWORD dwFlags =0L; BOOL bResult =FALSE; LPVOID lpPhysical=NULL; __PAGE_INDEX_MANAGER* lpIndexMgr=NULL; DWORD dwPteFlags=NULL; if((NULL==lpThis) || (0==dwSize)) //Parameter check. return NULL; if(VIRTUAL_AREA_ALLOCATE_IO !=dwAllocFlags) //Invalidate flags. return NULL; lpIndexMgr=lpMemMgr->lpPageIndexMgr; if(NULL==lpIndexMgr) //Validate. return NULL;
上述代码完成了本地变量的定义、参数合法性检查等工作,以确保参数的合法性。
lpStartAddr=(LPVOID)((DWORD)lpStartAddr & ~(PAGE_FRAME_SIZE-1)); //Round up to page. lpEndAddr =(LPVOID)((DWORD)lpDesiredAddr+dwSize ); lpEndAddr =(LPVOID)(((DWORD)lpEndAddr & (PAGE_FRAME_SIZE-1)) ? (((DWORD)lpEndAddr & ~(PAGE_FRAME_SIZE-1))+PAGE_FRAME_SIZE-1) : ((DWORD)lpEndAddr-1)); //Round down to page. dwSize =(DWORD)lpEndAddr-(DWORD)lpStartAddr+1; //Get the actually size.
上述代码把起始地址(应用程序可以指定一个期望预留的起始地址)舍入到页面长度边界,并根据长度计算预留的虚拟地址空间的结束地址,计算出结束地址后,也舍入到页面边界,最后计算出实际预留长度(因为经过上面两次舍入,预留长度可能会变化)。
lpVad=(__VIRTUAL_AREA_DESCRIPTOR*)KMemAlloc(sizeof(__VIRTUAL_AREA_ DESCRIPTOR), KMEM_SIZE_TYPE_ANY); //In order to avoid calling KMemAlloc routine // in the critical section,we first call it here. if(NULL==lpVad) //Can not allocate memory. goto __TERMINAL; lpVad->lpManager =lpMemMgr; lpVad->lpStartAddr =NULL; lpVad->lpEndAddr =NULL; lpVad->lpNext =NULL; lpVad->dwAccessFlags=dwAccessFlags; lpVad->dwAllocFlags =dwAllocFlags; __INIT_ATOMIC(lpVad->Reference); lpVad->lpLeft =NULL; lpVad->lpRight =NULL; if(lpVaName) { if(StrLen((LPSTR)lpVaName) > MAX_VA_NAME_LEN) lpVaName[MAX_VA_NAME_LEN-1]=0; StrCpy((LPSTR)lpVad->strName[0],(LPSTR)lpVaName); //Set the virtual area's name. } else lpVad->strName[0]=0; lpVad->dwCacheFlags=VIRTUAL_AREA_CACHE_IO;
上述代码调用KMemAlloc函数创建了一个虚拟区域描述符对象,并根据应用程序提供的参数,对虚拟区域描述符对象进行了初始化。
// //The following code searchs virtual area list or AVL tree,to check if the lpDesiredAddr //is occupied,if so,then find a new one. // __ENTER_CRITICAL_SECTION(NULL,dwFlags); if(lpMemMgr->dwVirtualAreaNum<SWITCH_VA_NUM) //Should search in the list. lpStartAddr=SearchVirtualArea_l((__COMMON_OBJECT*)lpMemMgr, lpStartAddr,dwSize); else //Should search in the AVL tree. lpStartAddr=SearchVirtualArea_t((__COMMON_OBJECT*)lpMemMgr, lpStartAddr,dwSize); if(NULL==lpStartAddr) //Can not find proper virtual area. { __LEAVE_CRITICAL_SECTION(NULL,dwFlags); goto __TERMINAL; }
上述代码根据目前虚拟区域描述符数量的大小,检索虚拟区域描述符链表或二叉树,以找到一个满足用户需求的虚拟区域。这时候有三种情况:一种情况是,能够找到一个满足用户需求大小的虚拟区域,但其起始地址跟用户提供的期望的起始地址不一致,此时VirtualAlloc仍然预留找到的虚拟区域,并把预留的虚拟区域的起始地址返回给用户;第二种情况是,查找到的虚拟区域完全满足用户的需求,即大小和起始地址都适合(用户期望预留的虚拟区域尚未被占用),这种情况下,VirtualAlloc也是以预留成功处理,返回用户预留的起始地址;最后一种情况是,若未能找到满足用户要求的结果(即系统线性空间中没有足够大的连续区域能够满足用户需求),则VirtualAlloc调用将以失败告终,返回用户一个NULL。
lpVad->lpStartAddr=lpStartAddr; lpVad->lpEndAddr =(LPVOID)((DWORD)lpStartAddr+dwSize-1); lpDesiredAddr =lpStartAddr; if(lpMemMgr->dwVirtualAreaNum < SWITCH_VA_NUM) InsertIntoList((__COMMON_OBJECT*)lpMemMgr,lpVad); //Insert into list or tree. else InsertIntoTree((__COMMON_OBJECT*)lpMemMgr,lpVad);
上述代码把找到的满足用户需求的虚拟区域插入虚拟区域描述符表或二叉树。
// //The following code reserves page table entries for the committed memory. // dwPteFlags=PTE_FLAGS_FOR_IOMAP; //IO map flags,that is,this memory //range will not use hardware cache. lpPhysical=lpStartAddr; while(dwSize) { if(!lpIndexMgr->ReservePage((__COMMON_OBJECT*)lpIndexMgr, lpStartAddr,lpPhysical,dwPteFlags)) { PrintLine("Fatal Error : Internal data structure is not consist."); __LEAVE_CRITICAL_SECTION(NULL,dwFlags); goto __TERMINAL; } dwSize-=PAGE_FRAME_SIZE; lpStartAddr=(LPVOID)((DWORD)lpStartAddr+PAGE_FRAME_SIZE); lpPhysical=(LPVOID)((DWORD)lpPhysical+PAGE_FRAME_SIZE); } __LEAVE_CRITICAL_SECTION(NULL,dwFlags); bResult=TRUE; //Indicate that the whole operation is successfully.
与注释指明的一样,上述代码完成了页表的预留,然后使用与线性地址相同的地址来填充页表。因为一次预留的虚拟区域的长度有可能是多个页面长度的倍数,因此上述代码是一个循环,每次循环都预留一个页表项,直到dwSize递减为0。
__TERMINAL: if(!bResult) //Process failed. { if(lpVad) KMemFree((LPVOID)lpVad,KMEM_SIZE_TYPE_ANY,0L); if(lpPhysical) PageFrameManager.FrameFree((__COMMON_OBJECT*)&PageFrameManager, lpPhysical, dwSize); return NULL; } return lpDesiredAddr; }
上述代码完成最后的处理,若整个操作失败(bResult为FALSE),则释放所有已经申请的资源,并返回NULL,否则,返回预留的虚拟区域的首地址。
需要注意的是,上述凡涉及共享变量的操作,都是在互斥对象保护下进行的,确保同时只有一个线程在进行相关操作,否则可能会导致数据不一致。
VIRTUAL_AREA_ALLOCATE_RESERVE
当调用者使用该标志调用VirtualAlloc时,说明调用者仅仅想预留一部分虚拟地址空间以备将来使用。
相关操作如下:
① 向下舍入lpDesiredAddr到FRAME_PAGE_SIZE边界,并在dwSize上增加舍入部分,然后向上舍入dwSize到FRAME_PAGE_SIZE边界;
② 检查从lpDesiredAddr开始、长度为dwSize的虚拟内存区域是否已经被分配,或者与已经被分配的虚拟区域重叠;
③ 如果没有分配,也没有重叠,则调用KMemAlloc函数创建一个新的虚拟区域描述对象(__VIRTUAL_AREA_DESCRIPTOR),根据调用参数等初始化该对象;
④ 如果上述区域已经被分配,或者与现有的已经分配的区域重叠,则VirtualAlloc重新寻找一块满足上述长度的连续区域,如果能够找到,则创建一个虚拟区域描述对象,并初始化,如果没找到,则说明虚拟内存空间已经被消耗完毕,直接返回NULL,调用失败;
⑤ 把上述区域描述对象插入虚拟区域链表或者AVL树,递增dwVirtualAreaNum,设置dwAllocFlags为VIRTUAL_AREA_ALLOCATE_RESERVE,并返回新创建的虚拟区域的初始地址。
上述功能的实现代码与VIRTUAL_AREA_ALLOCATE_IO类似,在此不再详述,不同的是,VIRTUAL_AREA_ALLOCATE_IO预留了页表项,而当以该标志调用VirtualAlloc时,却没有预留页表项,仅仅返回预留的虚拟区域的首地址。这时候若引用这个地址,会引发内存访问异常。
VIRTUAL_AREA_ALLOCATE_COMMIT
当使用该参数调用VirtualAlloc时,说明用户先前已经预留了虚拟内存空间(通过VIRTUAL_AREA_ALLOCATE_RESERVE调用VirtualAlloc函数)。本次调用的目的是想为先前已经预留的虚拟地址空间具体分配物理内存。这个时候lpDesiredAddr必须不能为NULL,否则直接返回。
相关操作如下:
① 遍历虚拟区域列表或AVL树,查找lpDesiredAddr是否已经存在,如果不能找到,则说明该地址没有被预留,直接返回NULL;
② 如果虚拟地址空间已经被预留,则判断预留大小。当前情况下,如果已经预留空间大小比dwSize大,则以预留的虚拟地址空间尺寸为准申请物理内存,否则(dwSize大于预留的虚拟空间大小)返回NULL,指示操作失败;
③ 调用PageFrameManager的FrameAlloc函数分配物理内存。按照当前的实现方式,只调用一次FrameAlloc函数为虚拟内存分配物理内存,这样由于调用一次FrameAlloc函数,最多可以分配的物理内存是8MB(目前的实现),所以,若采用本标志调用VirtualAlloc函数,目标虚拟区域的大小不能大于8MB,否则会失败;
④ 物理内存分配成功之后,调用ReservePage为新分配的物理内存以及虚拟内存建立对应关系;
⑤ 如果上述操作一切顺利,则设置虚拟区域描述符的dwAllocFlags值为VIRTUAL_AREA_ALLOCATE_COMMIT,返回lpDesiredAddr,否则返回NULL,指示分配失败。
在这种情况下,可实现一种叫做“按需分配”的内存分配策略,即开始时为调用者分配部分内存,比如一个物理内存页,此时,如果用户访问没有分配物理内存的虚拟内存地址空间,就会引发一个访问异常,系统的页面异常处理程序会被调用。异常处理程序会继续为用户分配需要的物理地址空间。
在当前的实现中,为提高系统的效率没有采用这种按需分配的内存分配策略,而是直接按照用户需求的大小分配内存页面,如果能够成功,则返回lpDesiredAddr指示操作成功,否则返回NULL,指示操作失败。这样就不会频繁地因内存访问异常导致系统效率大大下降。
VIRTUAL_AREA_ALLOCATE_ALL
当设定VIRTUAL_AREA_ALLOCATE_ALL标志调用VirtualAlloc时,其效果与用VIRTUAL_AREA_ALLOCATE_RESERVE和VIRTUAL_AREA_ALLOCATE_COMMIT联合调用效果相同。
相关操作流程如下:
① 向下舍入lpDesiredAddr到FRAM_PAGE_SIZE边界,对dwSize增加舍入数值,并向上舍入dwSize到PAGE_FRAME_SIZE边界。
② 检查虚拟区域链表或AVL树,以确定以lpDesiredAddr为起始地址、dwSize为长度的虚拟内存区域是否已经分配,或者是否与已经分配的虚拟区域重合。
③ 如果上述区域没有分配,也没有重合,则创建一个新的虚拟区域描述对象,根据lpDesiredAddr、dwSize等数值初始化该虚拟区域描述对象。
④ 如果上述虚拟区域已经分配,或者与现有的虚拟区域重合,则VirtualAlloc重新寻找一块虚拟区域(长度为dwSize),如果寻找成功,则设置lpDesiredAddr为新寻找的区域的起始地址,创建并初始化虚拟区域描述对象。
⑤把上述新创建的虚拟区域对象插入虚拟区域链表或AVL树,根据dwVirtualAreaNum决定具体插入哪个数据结构。上述对虚拟区域链表或AVL树的检查、虚拟区域描述对象(__VIRTUAL_AREA_DESCRIPTOR)的创建、虚拟区域插入链表或AVL树等操作,构成一个原子操作(关闭中断、SpinLock保护等),以保证链表或AVL树的完整性。
⑥ 调用PageFrameManager的AllocFrame函数,分配一块大小可以容纳dwSize的物理内存区域,如果分配成功,把取得的物理内存的地址存放在一个变量中,假设为lpPhysicalAddr。
⑦ 如果分配不成功,则重新调用AllocFrame函数分配一页物理内存(PAGE_FRAME_SIZE),并存放在lpPhysicalAddr变量内。如果分配失败,转失败处理流程。
⑧ 根据分配的物理内存的大小,调用PageIndexMgr的ReservePage函数完成页表的填充(完成虚拟地址和物理地址的映射)。如果分配的物理内存大小等于或超过dwSize,则以FRAME_PAGE_SIZE为单位,每次完成ReservePage函数后递增lpDesiredAddr变量和lpPhysicalAddr变量(因为ReservePage每次填充一个页面),并递减dwSize,直到dwSize为0为止。如果分配的物理内存的尺寸小于dwSize(只有FRAME_PAGE_SIZE)大小,则只在第一次调用ReservePage时,给出物理内存,后续的调用,只给出虚拟内存地址,并设定PTE_FLAG_NOTPRESENT标志,以指示内存尚未分配,这个循环,也是直到dwSize递减到0(FRAME_PAGE_SIZE为递减单位为止)。
⑨上述所有操作成功完成之后,设置目标虚拟区域描述符的dwAllocFlags标志为VIRTUAL_AREA_ALLOCATE_COMMIT,返回lpDesiredAddr作为成功标志,否则转失败处理流程(以下步骤)。
⑩ 如果处理过程转到该步骤及以下步骤,说明操作过程中发生了错误,需要还原到系统先前的状况,并释放所有已经分配的资源。
⑪ 检查是否已经分配虚拟区域描述符对象,如果已经分配,则从链表或AVL树中删除该对象,并释放该对象。
⑫ 如果已经分配物理内存,则调用PageFrameManager的FreeFrame函数释放物理内存。
⑬ 如果出现预留页表项不成功的情况,则说明系统内部出现问题(数据不连续),这个时候直接给出严重警告并停机。
上述功能的实现代码如下,为了方便理解,我们分段解释:
static LPVOID DoReserveAndCommit(__COMMON_OBJECT* lpThis, LPVOID lpDesiredAddr, DWORD dwSize, DWORD dwAllocFlags, DWORD dwAccessFlags, UCHAR* lpVaName, LPVOID lpReserved) { __VIRTUAL_AREA_DESCRIPTOR* lpVad =NULL; __VIRTUAL_MEMORY_MANAGER* lpMemMgr = (__VIRTUAL_MEMORY_ MANAGER*)lpThis; LPVOID lpStartAddr=lpDesiredAddr; LPVOID lpEndAddr =NULL; DWORD dwFlags =0L; BOOL bResult =FALSE; LPVOID lpPhysical=NULL; __PAGE_INDEX_MANAGER* lpIndexMgr=NULL; DWORD dwPteFlags=NULL; if((NULL==lpThis) || (0==dwSize)) //Parameter check. return NULL; if(VIRTUAL_AREA_ALLOCATE_ALL !=dwAllocFlags) //Invalidate flags. return NULL; lpIndexMgr=lpMemMgr->lpPageIndexMgr; if(NULL==lpIndexMgr) //Validate. return NULL; lpStartAddr=(LPVOID)((DWORD)lpStartAddr & ~(PAGE_FRAME_SIZE-1)); //Round up to page. lpEndAddr =(LPVOID)((DWORD)lpDesiredAddr+dwSize ); lpEndAddr =(LPVOID)(((DWORD)lpEndAddr&(PAGE_FRAME_SIZE-1))? (((DWORD)lpEndAddr & ~(PAGE_FRAME_SIZE-1))+PAGE_FRAME_SIZE-1) : ((DWORD)lpEndAddr-1)); //Round down to page. dwSize=(DWORD)lpEndAddr-(DWORD)lpStartAddr+1; //Get the actually size. lpVad=(__VIRTUAL_AREA_DESCRIPTOR*)KMemAlloc(sizeof(__VIRTUAL_ AREA_DESCRIPTOR), KMEM_SIZE_TYPE_ANY); //In order to avoid calling KMemAlloc // routine in the critical section,we first // call it here. if(NULL==lpVad) //Can not allocate memory. { PrintLine("In DoReserveAndCommit: Can not allocate memory for VAD."); goto __TERMINAL; } lpVad->lpManager =lpMemMgr; lpVad->lpStartAddr =NULL; lpVad->lpEndAddr =NULL; lpVad->lpNext =NULL; lpVad->dwAccessFlags =dwAccessFlags; lpVad->dwAllocFlags = VIRTUAL_AREA_ALLOCATE_COMMIT; //dwAllocFlags; __INIT_ATOMIC(lpVad->Reference); lpVad->lpLeft =NULL; lpVad->lpRight =NULL; if(lpVaName) { if(StrLen((LPSTR)lpVaName) > MAX_VA_NAME_LEN) lpVaName[MAX_VA_NAME_LEN-1]=0; StrCpy((LPSTR)lpVad->strName[0],(LPSTR)lpVaName); //Set the virtual area's name. } else lpVad->strName[0]=0; lpVad->dwCacheFlags=VIRTUAL_AREA_CACHE_NORMAL;
上述代码与VIRTUAL_AREA_ALLOCATE_IO相同,完成参数的检查、虚拟区域描述符的分配以及初始化等工作。
lpPhysical = PageFrameManager.FrameAlloc((__COMMON_OBJECT*)&Page FrameManager, dwSize, 0L); //Allocate physical memory pages.In order to reduce the time //in critical section,we allocate physical //memory here. if(NULL==lpPhysical) //Can not allocate physical memory. { PrintLine("In DoReserveAndCommit: Can not allocate physical memory."); goto __TERMINAL; }
上述代码完成实际的物理内存分配功能,调用FrameAlloc函数,并以最终计算的dwSize为大小来申请物理内存。若申请成功,则继续下一步的操作,否则,会打印出“无法分配物理内存”的信息,并跳转到该函数的最后,这样会导致该函数以失败告终,返回用户一个NULL。
// //The following code searchs virtual area list or AVL tree,to check if the lpDesiredAddr is occupied,if so,then find a new one. // lpEndAddr=lpStartAddr; //Save the lpStartAddr,because the //lpStartAddr may changed after // the SearchVirtualArea_X is called. __ENTER_CRITICAL_SECTION(NULL,dwFlags); if(lpMemMgr->dwVirtualAreaNum < SWITCH_VA_NUM) //Should search in the list. lpStartAddr=SearchVirtualArea_l((__COMMON_OBJECT*)lpMemMgr, lpStartAddr,dwSize); else //Should search in the AVL tree. lpStartAddr=SearchVirtualArea_t((__COMMON_OBJECT*)lpMemMgr, lpStartAddr,dwSize); if(NULL==lpStartAddr) //Can not find proper virtual area. { __LEAVE_CRITICAL_SECTION(NULL,dwFlags); goto __TERMINAL; }
上述代码查找虚拟区域描述符链表或二叉树,试图找到一个满足需要的虚拟内存区域,若查找失败,则会导致该函数以失败返回。需要注意的是,该操作也尝试以用户提供的期望地址为用户分配虚拟内存区域,若尝试失败,则选择另外一个满足大小,但不是用户期望地址的虚拟区域,返回给用户。
lpVad->lpStartAddr=lpStartAddr; lpVad->lpEndAddr =(LPVOID)((DWORD)lpStartAddr+dwSize-1); if(!(lpStartAddr==lpEndAddr)) //Have not get the desired area. lpDesiredAddr =lpStartAddr; if(lpMemMgr->dwVirtualAreaNum < SWITCH_VA_NUM) InsertIntoList((__COMMON_OBJECT*)lpMemMgr,lpVad); //Insert into list or tree. else InsertIntoTree((__COMMON_OBJECT*)lpMemMgr,lpVad);
上述代码把符合用户需求的虚拟区域插入到虚拟区域描述符链表或二叉树。
// //The following code reserves page table entries for the committed memory. // dwPteFlags=PTE_FLAGS_FOR_NORMAL; //Normal flags. while(dwSize) { if(!lpIndexMgr->ReservePage((__COMMON_OBJECT*)lpIndexMgr, lpStartAddr,lpPhysical,dwPteFlags)) { PrintLine("Fatal Error : Internal data structure is not consist."); __LEAVE_CRITICAL_SECTION(NULL,dwFlags); goto __TERMINAL; } dwSize-=PAGE_FRAME_SIZE; lpStartAddr=(LPVOID)((DWORD)lpStartAddr+PAGE_FRAME_SIZE); lpPhysical=(LPVOID)((DWORD)lpPhysical+PAGE_FRAME_SIZE); } __LEAVE_CRITICAL_SECTION(NULL,dwFlags); bResult=TRUE; //Indicate that the whole operation is successfully.
上述代码完成虚拟内存地址和物理内存地址之间的映射,即调用PageIndexManager提供的接口函数创建页表。操作成功完成后,设置bResult为TRUE,这表示该函数最终操作成功。
__TERMINAL: if(!bResult) //Process failed. { if(lpVad) KMemFree((LPVOID)lpVad,KMEM_SIZE_TYPE_ANY,0L); if(lpPhysical) PageFrameManager.FrameFree((__COMMON_OBJECT*)&PageFrame Manager, lpPhysical, dwSize); return NULL; } return lpDesiredAddr; }
上述代码是该函数的最后处理代码,根据bResult的结果做不同的处理,若bResult为TRUE,则说明一切操作成功,返回成功预留的虚拟地址,否则释放一切内存资源,包括虚拟区域描述符占用的资源、申请的物理页面等,然后返回NULL。
上述操作也可采用“按需分配”的原则,即如果用户请求的内存数量太大,则暂缓分配全部物理内存而是只分配一个物理页面,这样后续用户访问未分配物理页面的虚拟内存时,会引发一个访问异常,然后在异常处理程序中,将继续为没有分配到物理内存的虚拟内存分配物理页面。
在当前版本的实现中,考虑到系统效率等因素,没有实现“按需分配”的内存分配策略,而是采用“一次全部分配”的原则,即一次分配所有需要的物理内存,如果成功,则设置页表,并返回成功标志,否则直接返回失败标志。用户应用程序可以尝试改变请求的大小,再次调用VirtualAlloc函数。
6.VirtualFree
该函数是VirtualAlloc的反向操作,用于释放调用VirtualAlloc函数分配的虚拟区域。该函数执行流程如下:
① 根据调用者提供的虚拟地址查找虚拟区域列表或AVL树,找到对应的虚拟区域描述符;
② 如果不能找到,则说明该区域不存在,直接返回;
③ 根据dwAllocFlags的不同取值完成不同的操作;
④ 如果dwAllocFlags的值是VIRTUAL_AREA_ALLOCATE_RESERVE,则仅仅从链表或AVL树中删除该虚拟区域描述符,并释放该虚拟区域描述符占用的内存,然后直接返回;
⑤ 如果dwAllocFlags的值是VIRTUAL_AREA_ALLOCATE_IO,则从链表中删除虚拟区域描述符,调用PageIndexMgr的ReleasePage函数,释放预留的页表,最后释放虚拟区域描述符占用的物理内存,并返回;
⑥ 如果dwAllocFlags的值是VIRTUAL_AREA_COMMIT,则说明已经为该虚拟区域分配了实际的物理内存,这种情况下,就需要释放物理内存。首先,从链表或AVL树中删除虚拟区域描述符对象,调用PageIndexMgr的GetPhysicalAddr函数,获取虚拟地址对应的物理地址、然后调用PageFrameManager的FreeFrame函数,释放物理页面;最后依次调用(以FRAME_PAGE_SIZE为递减单位递减dwSize,直到dwSize为0为止)PageIndexMgr的ReleasePage函数,释放预留的页表。所有这些操作完成之后,函数返回。
需要注意的是,上述所有操作,包括对虚拟区域链表或AVL树的删除、页表的释放等操作,都需要在一个原子操作内完成,以免发生系统级的数据结构不一致。实现该部分功能的代码比较简单,在此不作详细描述。
7.GetPdeAddress
该函数返回页目录的物理地址用于设置CPU的特定寄存器,比如针对Intel的IA32构架CPU需要设置CR3寄存器,这时候就需要知道页目录的物理地址。
另外,该函数直接读取PageIndexMgr的页目录物理地址,并返回给调用者。