WinCE 编程实验(第四章 储存管理)
来源:互联网 发布:红手指同类软件 编辑:程序博客网 时间:2024/05/20 14:17
第四章 储存管理
嵌入式系统与一般计算机系统中所使用的储存管理 (storage management) 有着明显的差异。在一般计算机系统中,因为有硬盘的支持,再加上内存容量的扩充性相当大,通常都使用硬盘做为储存系统 (storage system) 中的后备储存体 (backup storage) 来储存包括操作系统和应用程序在内的档案,内存则用来储存执行程序及所需数据。在嵌入式系统中,因其所强调的是轻便性与可移植性,一般没有硬盘的支持,且内存容量的扩充性相对而言相当有限。因此,如何设计一个高效能的储存管理,对有限的内存资源进行公平有效的管理,在嵌入式系统中就显得更为重要。在本章中,我们将对Windows CE中,储存管理采用的内存分配、回收、虚拟内存映像、调页等机制一一作深入的探讨。
4.1 物理内存结构
在Windows CE系统中,内存的实体结构大致可分为RAM和ROM二种内存。
RAM
RAM的全名为Random Access Memory。在一般计算机系统中,所谓内存容量即指RAM的大小而言。如前所述,RAM在一般计算机系统中,专用来储存执行程序及所需数据。在程序执行的流程中,操作系统先将程序从硬盘里加载到RAM中,CPU即执行存放在RAM中的程序,程序执行时所需的heap和stack,也都分别位在RAM中不同的地址上。
在Windows CE系统中,RAM被分为程序空间 (program memory) 和对象空间 (object store)。程序空间和一般计算机系统中RAM的使用类似,用来储存执行程序及所需数据。对象空间则类似一般计算机系统中的硬盘,用来储存应用程序及档案。对象空间中存放的数据可分为三大类 : (1) 档案系统 (file system),(2) 注册信息(registry),(3) Windows CE数据库 (Windows CE database)。程序空间和对象空间的大小是可以调整的。若是Windows CE发现处理程序 (process) 的内存空间不敷使用的情况下,Windows CE会要求使用者释放一些对象空间的内存来满足程序空间的需求。图4.1是Windows CE 3.0在Compaq iPAQ H3850所提供的内存空间配置画面,此系统中可使用之RAM大小为63.14 MB,其中34.65 MB为程序空间,剩余的28.49 MB则用做对象空间。程序空间的使用部分为7.27 MB,而对象空间的使用部分则为0.52 MB。
图4.1 Compaq iPAQ H3850 内存空间配置程序
RAM内存既是电子装置,其所储存的数据则须靠电力来维持。换言之,电力来源一但中断,储存在RAM中的数据就消失了。一般计算机系统中电力的供应来自外接电源,一但停电或是关机,电力供应中断,RAM中储存的执行程序及数据马上消失。至于笔记型计算机,其电力虽靠电池提供,但在关机状态时,电池的电力供应是完全切断的,因此储存在RAM中的程序及数据,也会随着关机而消失。
在Windows CE系统中,RAM既被用来当作对象空间使用,电力的来源则靠电池提供,因此有效的使用有限的电力来维持RAM中所储存的数据,则显的更为重要。Windows CE系统的电源管理机制,即使在关机状态时,仍旧有少量的电力供应RAM,以保留储存在其中的数据。只有在电池电力完全耗尽时,RAM中储存的数据才会消失,而当系统重新获得电力时,一切的数据与软件都必需新加载。市场上常见的Compaq iPAQ,当电池电力完全耗尽而重获电力启动后,使用者都必需重新输入使用者数据,或是做一些校正的动作,就是一个明显的例子。
ROM
ROM的全名为Read Only Memory,又称只读存储器,故名思义,ROM中储存的内容只能被读取,而不能有写入或更改的动作。因为ROM不需电力的供应,仍然能保有储存的内容,因此在嵌入式系统中,常被用来储存操作系统,或是重要且不会更改的数据 (如开机时需要的数据)。
在Windows CE系统中,ROM储存了包含操作系统在内相关的档案。系统程序执行则分为两种模式 : RAM执行模式和ROM执行模式。一般的系统程序执行采用RAM执行模式,操作系统会先把程序从ROM中取出,加载RAM中执行。RAM执行模式因为牵涉到程序拷贝,有执行效率差及占用RAM内存空间等缺点。为此,Windows CE提供XIP (Execute in Place),也就是ROM执行模式,使得储存在ROM中的系统程序,可以在指定的ROM地址中直接执行,而不用加载RAM中。
ROM的只读特性,使得更改其中内容这个动作,变的相当复杂。举例来说,若Compaq iPAQ只具有ROM作为操作系统的储存工具,要升级一个Compaq iPAQ的操作系统,必需先有一颗新的ROM,利用特殊的设备将新的操作系统写入,再用此ROM更换iPAQ中原有的ROM。因为此特殊的设备甚为昂贵且不易取得,近年来便有Flash ROM的产生。Flash ROM不仅保有原本ROM在电源消失后仍保有数据的好处,并且拥有RAM可写入的特性,因此我们可以用它来保存操作系统,并利用软件的方式来升级或更换操作系统。Compaq iPAQ H3600系统的PDA中所配有的ROM即是Flash ROM。使用者可以到Compaq或相关网站取得Flash ROM写入程序,并利用此程序来进行操作系统升级或更换 (例如Windows CE ßà Linux) 的动作,而完全无需更换硬件。
4.2 虚拟内存管理
Windows CE系统的内存管理是建构在虚拟内存机制上。在本节中,我们将介绍Windows CE的虚拟内存机制、其中所使用的分页机制、及提供多任务作业的slot结构。
虚拟内存
计算机程序的运算大多是在循环 (loop) 结构内完成。循环执行时所表现出来的地址局限性 (address locality) 及空间局限性 (space locality),使得执行程序所需的物理内存空间远比程序代码本身小,且同一时段内,只需加载一小部分的数据结构即可。为了程序设计师撰写程序方便,操作系统一般都将程序内存空间与物理内存空间分开。这里所称的程序内存即是虚拟内存。物理内存内所使用的地址称为实体地址 (physical address),而虚拟内存内所使用的地址称为虚拟地址 (virtual address)。
虚拟内存的大小可以远大于物理内存的大小。在一般操作系统中,虚拟内存的大小决定于所使用的地址位长度。举例而言,在一个32-bit地址长度的操作系统,其虚拟内存的大小为4GB (232 Bytes),而系统所配备的物理内存可能只有32MB或64MB。虚拟内存的设计,使得程序设计师撰写程序时,不必顾虑到系统的物理内存的大小。另外,虚拟内存允许不同程序共享相同的档案和程序代码。图4.2为虚拟内存的概述图,其中说明了虚拟地址和实体地址的转换。
分页机制
Windows CE系统所使用的地址位长度为32-bit。换言之,其虚拟内存的大小为4GB (232 Bytes) 。Windows CE将这4GB虚拟地址空间分为低地址2GB及高地址2GB。应用程序使用的地址空间为低地址2GB,高地址2GB专供Windows CE核心模块使用。Windows CE操作系统使用KDataStruct数据结构来存放低地址2GB内的数据。程序代码4.1列出KDataStruct整个数据结构。
程序代码4.1 KDataStruct数据结构
struct KDataStruct {
LPDWORD lpvTls; /* 0x000 Current thread local storage pointer */
HANDLE ahSys[NUM_SYS_HANDLES]; /* 0x004 If this moves, change kapi.h */
char bResched; /* 0x084 reschedule flag */
char cNest; /* 0x085 kernel exception nesting */
char bPowerOff; /* 0x086 TRUE during "power off" processing */
char bProfileOn; /* 0x087 TRUE if profiling enabled */
ulong ptDesc; /* 0x088 Page Table Descriptor */
ulong rsvd2; /* 0x
PPROCESS pCurPrc; /* 0x090 ptr to current PROCESS struct */
PTHREAD pCurThd; /* 0x094 ptr to current THREAD struct */
DWORD dwKCRes; /* 0x098 */
ulong handleBase; /* 0x
PSECTION aSections[64]; /* 0x
LPEVENT alpeIntrEvents[SYSINTR_MAX_DEVICES]; /* 0x
LPVOID alpvIntrData[SYSINTR_MAX_DEVICES]; /* 0x220 */
ulong pAPIReturn; /* 0x
uchar *pMap; /* 0x
DWORD dwInDebugger; /* 0x
PTHREAD pCurFPUOwner; /* 0x
PPROCESS pCpuASIDPrc; /* 0x2b0 current ASID proc */
long alPad[19]; /* 0x2b4 - padding */
DWORD aInfo[32]; /* 0x300 - misc. kernel info */
/* 0x380 - interlocked api code */
/* 0x400 - end */
}; /* KDataStruct */
在KDataStruct数据结构中,又利用PSECTION aSections[64] 将低地址2GB分割成64个32MB大小的空间,称之为Section。一个Section再被分割成512个64KB大小的空间,称之为MemBlock,如程序代码4.2所示。一个MemBlock再被分割成数个页 (Page)。页的大小 (PAGE_SIZE) 在不同的系统中略有不同。ARM4处理器的PAGE_SIZE为4096,ARM920的PAGE_SIZE为1024,MIPS及x86处理器的PAGE_SIZE则为4096。若以PAGE_SIZE = 4096,一个MemBlock可被分割成16个页。程序代码4.3列出MemBlock整个数据结构,其中aPages[PAGES_PER_BLOCK] 字段纪录虚拟内存中每一个页所对应到的物理内存地址。
程序代码4.2 BLOCK、SECTION数据结构
#define BLOCK_MASK 0x1FF
typedef MEMBLOCK *SECTION[BLOCK_MASK+1];
//每一个SECTION指向512个BLOCK
typedef SECTION *PSECTION;
程序代码4.3 MemBlock数据结构
#define PAGE_SIZE 4096 /* page size */
#define PAGES_PER_BLOCK (0x10000 / PAGE_SIZE)
struct MemBlock {
ACCESSLOCK alk; /* 00: key code for this set of pages */
uchar cUses; /* 04: # of page table entries sharing this leaf */
uchar flags; /* 05: mapping flags */
short ixBase; /* 06: first block in region */
short hPf; /* 08: handle to pager */
short cLocks; /*
ulong aPages[PAGES_PER_BLOCK];
}; /* MemBlock */
我们以CEPC为例子来说明虚拟内存到物理内存的对映关系。CEPC是将Windows CE操作系统在个人计算机上执行。在这个以x86处理器的平台上,虚拟内存到物理内存对映,可以用图4.5及图4.6来解释。应用程序执行时所送出的地址,即虚拟地址,长为32个位 (编号第31-0个位)。一个虚拟地址可以如图4.5所示切割成五大区段 :
l 第一区段只有一个位,即第31个位,因应用程序只能使用低地址2GB,第31个位一定是0。
l 第二区段有六个位,即第30-25个位,纪录此地址所属的Section号码。
l 第三区段有九个位,即第24-16个位,纪录此地址所属的MemBlock号码。
l 第四区段有四个位,即第15-12个位,纪录此地址所属的页号码。
l 第五区段有十二个位,即第11-0个位,纪录此地址在所对映的物理内存页内的相对地址。
31 30 24 15 11 0
0 | Section number 6位 | Block number 9位 | Page number 4位 | Offset 12位 |
图4.5 x86处理器虚拟地址的划分
Windows CE操作系统在收到一个虚拟地址后,即根据图4.5所示将此地址切割,并根据每区段内的值,从程序代码4.1的KDataStruct开始,依Section、MemBlock、页的顺序,来找到此虚拟地址所对映到的物理内存地址。整个对映的流程图示在图4.6中。
Windows CE从低地址的2GB起始地址开始的连续地址,划分出33个32MB的内存空间,称之为”slot”,等同于前述的前33个SECTION,当处理程序要执行前,Windows CE将处理程序加载至slot中执行,特别需要注意的是,slot 0保留给目前正在执行的处理程序使用,其余的slot则留给非正在执行的处理程序使用。因此,Windows CE系统中,最多只能有32个处理程序同时进行。图4.3是内存的配置情形,稍后将介绍memory mapping的细节及其它的内存空间。
2GB
slot 0 | slot 1 | slot 2 | … | slot 32 | For Memory Mapping | 为操作系统保留的内存空间 |
图4.3
于Windows CE PlatformBuilder使用mi指令可获得CEPC内存信息,图4.4为mi指令的使用方式及结果。
图
程序代码4.4为mi指令执行后的片段结果:
程序代码4.4 CEPC内存信息
Windows CE Kernel Memory Usage Tool 0.2
Page size=4096, 4958 total pages, 4544 free pages. 4517 MinFree pages (1806336 MaxUsed bytes)
90 pages used by kernel, 0 pages held by kernel, 414 pages consumed.
Memory usage for Process 81791560: 'NK.EXE' pid 0
SECURE SECTION:
Slot base c2000000 Section ptr 81749d00 //为系统保留的
Page summary: code=0(0) data r/o=0 r/w=2 stack=3 reserved=315 //SECURE SECTION
ROM DLL CODE SECTION:
Slot base 02000000 Section ptr 83ffc000 //Slot 1 的使用情形
Page summary: code=508(0) data r/o=68 r/w=1 stack=0 reserved=2672
Memory usage for Process 81791614: 'filesys.exe' pid 1 //Slot 2被filesys.exe
Slot base 04000000 Section ptr 83ff1000 //使用情形
Page summary: code=31(0) data r/o=1 r/w=20 stack=5 reserved=334
Memory usage for Process
Slot base 06000000 Section ptr 83fcf000 //使用情形
Page summary: code=10(0) data r/o=0 r/w=7 stack=2 reserved=330
Memory usage for Process
Slot base 08000000 Section ptr 83fc9000 //device.exe使用
Page summary: code=6(1) data r/o=0 r/w=113 stack=40 reserved=958 //情形
SLOT 0
由于在Windows CE系统中,许多处理程序都会拥有相同的DLLs (dynamic link libraries) 档,若是每一个处理程序都在物理内存找出一块空间放置相同的DLLs档,由于DLLs的程序代码是只读性且不可变更的,若是重复放置将会是物理内存的一大浪费,因此Windows CE系统将不同处理程序的相同DLLs对应到同样的物理内存地址,如此减少了内存的浪费,虽然DLLs档是相同的,每个处理程序仍旧必需拥有一些自己的DLLs变量数据,因为DLLs变量数据并不是相同的。此时,产生了一个问题,DLLs档如何知道目前是哪一个处理程序在呼叫它?Windows CE为了解决这个问题,规定DLLs档只能对slot 0的变量做变动,也就是目前正在执行的处理程序,这也是slot 0存在的原因。
DLL xxx DLL xxx DLL Code 虚拟内存 物理内存 slot 1 slot 2 slot 0 Current Process
DoVirtualAlloc() 函式
Windows CE系统配置内存有两种方式 : 保留(MEM_RESERVE) 和提交(MEM_COMMIT)。这两种方式的差别在于保留只会保留虚拟页给处理程序,而提交不仅给予处理程序虚拟页,更会于物理内存取得对应页。
DoVirtualAlloc() 函式可以根据使用者对内存的要求,如保留、提交,执行保留虚拟内存或是抓取实体页并填入页表 (page table) ,由于原始码非常复杂,本章节提供流程图 (图4.6) 供读者参考,其原始码的位置在于 : WINCEROOT/PRIVATE/WINCEOS/COREOS/NK/KERNEL/virtmem.c 有兴趣的读者可以查阅。
否,在堆栈增长时 自动提交 不通过 否 否 将页表项 标记为保留 合法性检查 进入临界区 地址? 是 分配系统保留段 是否需要提交? 对齐地址到64K边界 检查所有块都已保留 是 报错 已在分配段 是 合法性检查 按给定的方式扫描可用内存区寻找满足条件的可用内存块 是否找到 回传错误退出 在系统堆栈中分配页表 (MEMBLOCK)空间 将当前处理程序的存取 控制讯息填入页表 页面是否需要映像到实体地址? 抓取足够的实体页 成功? 将得到的实体 页框填入页表 报错退出 是 是 是 否 否 是否为安全地址? 地址是否已经保留?
图4.6虚拟地址分配流程
4.3物理内存管理
可用内存链接串行
可用内存串行用以记录目前系统中尚可使用的内存,可由LogPtr->pKList找到串行。可用内存串行采用简单的双向链接串行形式。所以链接串行的管理极为简单。程序代码4.5是插入可用内存链接串行的程序代码:
程序代码4.5 插入可用内存链接串行程序代码
void
LinkPhysPage(
LPBYTE pMem
)
{
KCALLPROFON(32);
*(LPBYTE *)((DWORD)pMem + 0x20000000) = LogPtr->pKList;
*(LPBYTE *)((DWORD)pMem + 0x20000004) = 0;
if (LogPtr->pKList)
*(LPBYTE *)((DWORD)LogPtr->pKList + 0x20000004) = pMem;
LogPtr->pKList = pMem;
PageFreeCount++;
LogPhysicalPages(PageFreeCount);
KCALLPROFOFF(32);
}
其中KCALLPROFON() 和KCALLPROFOFF() 分别是开启、关闭与加入可用内存串行相关的系统记录。
取得物理内存
图4.6中,取得物理内存是利用GetHeldPage函式呼叫GrabFirstPhysPage函式取得实体页,程序代码4.6是GetHeldPage函式,程序代码4.7是GrabFirstPhysPage()函式部分程序代码。
程序代码4.6 GetHeldPage函式
PHYSICAL_ADDRESS GetHeldPage(void)
{
PHYSICAL_ADDRESS paRet;
LPBYTE pMem;
if (pMem = (LPBYTE)KCall((PKFN)GrabFirstPhysPage,1)) { //GrabFirstPhysPage取得
PageFreeCount++; // since we already reserved it //实体页回传
LogPhysicalPages(PageFreeCount);
paRet = GetPFN(pMem);
} else
paRet = 0;
DEBUGMSG(ZONE_PHYSMEM,(TEXT("GetHeldPage: Returning %8.8lx/r/n"), paRet));
return paRet;
}
程序代码4.7 GrabFirstPhysPage函式
LPBYTE
GrabFirstPhysPage(
DWORD dwCount
)
{
PFREEINFO pfi;
uint ix;
PHYSICAL_ADDRESS paRet;
LPBYTE pMem;
KCALLPROFON(33);
//检查PageFreecount (可用实体页)的值是否不小于0,而不是检测LogPtr->pKList
//是否为空,因为后者可能导致PageFreecount<0
if (PageFreeCount) {
pMem = LogPtr->pKList;
paRet = GetPFN(pMem);
//从实体页框得到系统可用内存区域指针
pfi = GetRegionFromAddr(paRet);
if (pfi) {
//
// 从可用内存链接串行中取出一页
// 不经过Cache
//
if (LogPtr->pKList = *(LPBYTE *)((DWORD)pMem + 0x20000000)) {
*(LPBYTE *)((DWORD)LogPtr->pKList + 0x20000004) = 0;
}
//
//去掉可用内存链接串行的信息,得到空白页
*(LPDWORD)(pMem + 0x20000000) = 0;
*(LPDWORD)(pMem + 0x20000004) = 0;
//修改系统中的FREEINFO变量的值,将引用数目改为dwCount
ix = (paRet - pfi->paStart) / PFN_INCR;
DEBUGCHK(pfi->pUseMap[ix] == 0);
DEBUGCHK(dwCount && (dwCount <= 0xff));
pfi->pUseMap[ix] = (BYTE)dwCount;
if (-- PageFreeCount < (long) KInfoTable[KINX_MINPAGEFREE]) {
KInfoTable[KINX_MINPAGEFREE] = PageFreeCount;
}
………………………………
return pMem;
}
偏移量0x20000000定义了一个映像,因为系统核心(kernel)也工作在虚拟内存模式下,该映像就将可用内存链接串行,保存在kernel的工作空间之中。所以,可用内存链接串行只是保留的实体值的指针。回收的过程与此类似,定义在FreePhysPage()中。
获得连续实体页的分配策略定义在GetContiguousPages中,其基本策略是扫描各个区域(Region),找到第一个满足条件的连续实体块。这个函数在执行时需要占用物理内存的临界区很长时间,严重影响其它要进入该临界区的处理程序的运行。
实体页抓取流程
在虚拟内存的映像过程中,要进行实体页的抓取,这个过程分为两个步骤,第一步是检查物理内存是否满足需求,如果配置导致物理内存极度减少,则启动CleanUp执行绪,并向系统发出警报。如果配置后实体页数低于某个阈值,则削减执行绪PthScavTarget的堆积(stack),如果可以满足,那么标记配置并警告GWE内存不足,这部分原始码定义在函数HoldPages中。该函数的流程见图4.7。
第二步,标记之后,由GetHeldPage函数得到所需的实体页,该函数主要呼叫GrabFirstPhysPage从可用内存链接串行中摘出一个实体页。
进入PHY临界区 否 剩余实体页是否小于触发临界值? 设置页面换出标志 设置换页执行绪优先级 向系统发出警报 是 否 能否满足需求? 削减PthScavTarget堆积 是 成功退出 强制配置? 失败退出 修改可用内存页数 PageFreeCount 设置GweOOM事件通知系统物理内存不足 成功退出 是 是 是 否 否 否 否 配置数大于警戒阈值并且配置后剩余实体页面小于警戒值 配置数大于低阈值并且配置后剩余实体页面小于低阈值?
图4.7 实体页面抓取流程
ProcessPageFault函式
ProcessPageFault函式处理存取页内存时发生的错误,产生的原因可能为:
.虚拟内存地址没有实体页映像
.存取权限不允许此动作
.无效虚拟内存地址
在ProcessPageFault函式定义了这几种情况下相应的处理方法。首先检查该虚拟内存地址是否有效,再检查当前处理程序是否对该地址有权限,最后根据该block的类型决定呼叫函式,如果是mapper则呼叫MapperPageIn,如果是处理程序地址空间则调用LoadPageIn,每次呼叫若不成功,则重复,2秒钟以后超时,传出错误讯息。
页错误处理的部分程序代码:
程序代码4.8 页错误处理的部分程序代码
retryPagein:
EnterCriticalSection(&VAcs);
pscn = IsSecureVa (addr)? &NKSection : SectionTable[addr >> VA_SECTION];
if ((pscn != NULL_SECTION) &&
((pmb = (*pscn)[ixBlock]) != NULL_BLOCK)) {
/*虚拟内存地址有效性检查*/
if (pmb == RESERVED_BLOCK) {
pmb = (*pscn)[FindFirstBlock(pscn, ixBlock)];
pagerType = pmb->flags & MB_FLAG_PAGER_TYPE;
} else {
if (pmb->aPages[ixPage] & PG_VALID_MASK) {
if (!bWrite || IsPageWritable(pmb->aPages[ixPage]))
bRet = TRUE;
else
pagerType = pmb->flags & MB_FLAG_PAGER_TYPE;
} else if (pmb->aPages[ixPage] != BAD_PAGE)
pagerType = pmb->flags & MB_FLAG_PAGER_TYPE;
}
if (TestAccess(&pmb->alk, &CurAKey)) {
/*处理程序对该虚拟内存地址的可存取性检查*/
if (pagerType != MB_PAGER_NONE) {
/*若该页没有分配,则传出错误讯息,退出*/
LeaveCriticalSection(&VAcs);
dwLast = KGetLastError(pCurThread);
pageresult = PageInFuncs[pagerType-MB_PAGER_FIRST](bWrite, addr0); //根据该虚拟内存段的类型确定调页函数MappedPageIn或者
//LoaderPageIn
KSetLastError(pCurThread,dwLast);
if (pageresult == PAGEIN_RETRY) {
Sleep(250);
bRet = FALSE;
pagerType = MB_PAGER_NONE;
goto retryPagein;
goto FailPageInNoCS;
}
bRet = (BOOL)pageresult;
EnterCriticalSection(&VAcs);
}
} else {
bRet = FALSE; // Thread not allowed access, return failure.
}
}
LeaveCriticalSection(&VAcs);
4.2 堆栈(Heap)
当处理程序执行时,需要静态或动态配置内存,如宣告变量或宣告指针,此时所需的内存小,不应配置一个页面而造成内存的浪费,堆栈因此而生,系统可以利用堆栈对处理程序做较少的内存配置,而使用者不需了解系统的内存配置。处理程序可以利用堆栈申请大小比页小很多的内存,一旦申请了堆栈,系统会根据处理程序的需要自动增加堆栈的大小,而释放堆栈的时候,堆栈的削减也是由系统自动完成的。在Windows CE中,系统对堆栈的管理只允许申请固定大小的块。这样做的好处是简化了对堆栈中各个块的管理,代价是在程序运行过程中不断的分配和释放会出现很多碎片,会导致只要堆栈没有被完全释放,还会保留大量的虚拟页的情况。在Windows CE中,每个堆栈都有一个信号(signal)来控制顺序存取,两个处理程序同时对堆栈执行动作是不允许的。
本地堆栈(Local Heap)和独立堆栈(Separate Heap)
在缺页情况下,系统保留384个页作为本地堆栈,这些页只有在分配的时候才会被提交,如果应用程序需要的本地堆栈超过384页,系统会自动分配其余的空间,不过这些空间可能在虚拟地址上并不相邻而产生碎裂,因为系统的堆栈分配只支持固定大小的块。
为了避免碎裂的出现,在申请要反复分配和回收的储存空间时,使用者最好对每个申请使用独立堆栈。独立堆栈的分配是以slot为界限的,系统不允许应用程序申请的堆栈耗尽其它处理程序所拥有的32MB虚拟地址空间。
堆栈的逻辑结构
逻辑地址是虚拟内存地址的名称,而逻辑结构就是虚拟地址的结构。处理程序分配储存空间的基本单位是堆栈。每个堆栈维护着一个虚拟地址项(VAitem)和区域(region)的链接串行。项的大小可变,是实际进行分配时的最小单位。项和项之间在虚拟地址上是相邻的,所以项只在链接串行头记录该项的大小,下一个项就可以顺着这个值找到。为了便于对项的管理,系统还定义了单位更高的区域,区域是一段连续的项的空间,也是虚拟地址分配和回收的基本单位,记录了区域中最大的项的大小以及所属的堆栈,这样在查找可用内存项的时候可以提高效率,并且可以用很大的单位申请虚拟地址空间和提交已保留的空间。虚拟地址项是为了在分配大空间时直接使用的,因为单位大,所以不必包含在区域中,而直接由堆栈来维护
下面是结构的定义:
程序代码4.9
struct region {
pitem pitFree; // 00: 下一个可用内存项
pregion prgnLast; // 04: 区域串行上最后一个区域指针(只对第一个区域有效)
int cbMaxFree; // 08: 该区域中最大可用内存项的尺寸
pitem pitLast; //
pheap phpOwner; // 10: 所属堆栈
pregion prgnNext; // 14: 该堆栈中下一个区域的指针
}; // 18: 区域大小
struct heap {
DWORD dwSig; // 00: 堆栈的符号“Heap”
pvaitem pvaList; // 04: vaitem的链接串行
pheap phpNext; // 08:堆栈链接串行中下一个堆栈
WORD flOptions; //
#ifdef MEMTRACKING
WORD wMemType; // 0E: 注册的内存类型
#else
WORD wPad; // 0E: 填充位,用作对齐
#endif
DWORD cbMaximum; // 10: 堆栈大小的上限 (0表示可增长)
CRITICAL_SECTION cs; // 14: 管理该堆栈使用的临界区
region rgn; // 28: 该堆栈中第一个储存区域
};
// Item structure:
// 区域中的项有如下几种类型:
// busy items: size > 0 prgn 指向item所属的区域
// free items: size < 0
// hole items: size > 0 and prgn == NULL
// end maker: size == 0 cbTail 表示尾部之前的字节数
// 最后一种表示项是由虚拟地址映像的。
// virtual item: size > 0 并且size为奇数
struct item {
int size; // 项的大小(包含头部和尾部)
union {
pregion prgn; // 项所属区域的指针
pheap php; // 或是指向包含虚拟储存项的堆栈指针
int cbTail; // 该区域中尾部前边的字节数
};
#if HEAP_SENTINELS
int cbTrueSize ; // 实际需要的字节数
DWORD dwSig; // 项头的统计讯息
#endif
};
struct vaitem {
pvaitem pvaFwd; // 堆栈中下一个虚拟地址项的指针
pvaitem pvaBak; // 堆栈中前一个虚拟地址项的指针
item it; // 项讯息
};
堆栈的创建和初始化
堆栈的创建和初始化定义在HeapCreate(DWORD flOptions, DWORD dwInitialSize, DWORD dwMaximumSize)函数中,flOptions是创建方式,dwInitialSize是初始分配尺寸,dwMaximumSize是堆栈的尺寸上限,如果为零,说明堆栈是可以增长的。在创建的过程中,系统先将dwInitialSize,dwMaximumSize都对齐到页的大小,然后呼叫VirtulAlloc保留dwMaximumSize个虚拟页,然后再呼叫InitNewHeap分配dwInitialSize个虚拟页并对这个堆栈进行初始化。初始化部分的程序代码如下:
程序代码4.10 堆栈初始化部分程序代码
InitializeCriticalSection(&php->cs); /*初始化该堆栈的存取临界区*/
// 初始化堆栈中第一个可用内存项
pit = FIRSTITEM(&php->rgn);
php->rgn.pitFree = php->rgn.pitLast = pit;
dwTemp = (dwInitialSize - SIZE_HEAP_HEAD - sizeof(item)) & ~(ALIGNBYTES-1);
pit->size = -(int)dwTemp; //将项大小置为整个分配空间的大小,
//并标记为可用内存(<0)
pit->prgn = &php->rgn;
pit = (pitem)((char*)pit + dwTemp); /*找到下一个项的起始地址*/
pit->size = 0; /*标记为结尾标识*/
pit->cbTail = cbRegion - dwInitialSize;
php->rgn.cbMaxFree = dwTemp; /*初始化该堆栈的第一个区域*/
php->rgn.phpOwner = php;
php->rgn.prgnLast = &php->rgn; /*设置链接串行指针*/
堆栈的分配和回收
堆栈的分配过程首先计算堆栈所在的slot,如果分配方式为可增长且分配空间大于188k,那么直接从该slot分配虚拟地址给pVAitem。否则在已有区域中找出满足条件的项,如果找不到则创建一个新的区域,将整个区域的空间细化,使之成为一个刚好满足条件的项。程序代码4.11为堆栈分配过程的部分程序代码:
程序代码4.11 堆栈分配过程的部分程序代码
if (!php->cbMaximum && (dwBytes+sizeof(vaitem) >= CE_VALLOC_MINSIZE)) {
pitRet = DoLargeHeapAlloc (lpSlot, php, dwBytes);
//如果分配的空间大于188k,则直接从虚拟内存分配空间给虚拟地址项。
} else {
int cbSeek = ALIGNSIZE(dwBytes + sizeof(item) + HEAP_SENTINELS);
// 进入该堆栈的临界区以控制存取的顺序性
EnterCriticalSection (&php->cs);
if (!(pitRet = FindFreeItem (lpSlot, php, cbSeek, &prgn)) && !php->cbMaximum
&& (prgn = CreateNewRegion (lpSlot, php, cbSeek))) {
// 如果不能在已有的区域内找到,则创建一个新的区域
pitRet = FIRSTITEM (prgn);
}
if (pitRet) {
// 将得到的可用内存项细化
pitRet = CarveItem (prgn, pitRet, cbSeek, dwBytes, NULL);
}
LeaveCriticalSection (&php->cs);
FindFreeItem是寻找第一个可用项的函数主要呼叫FindFreeItemInRegion,使用的算法描述如下。
do {
while ((cbItem = pit->size) > 0) { /*该项在使用中?*/
pit = (pitem)((char*)pit + cbItem);
}
pit = MergeFreeItems (prgn, pitRet = pit); /*如果有相邻可用内存项,则合并之*/
cbItem = -pitRet->size;
// 判断这个块是否够大
if (cbItem >= cbSeek) {
if (cbItem - cbSeek > cbMax)
cbMax = cbItem - cbSeek;
break;
}
if (cbMax < cbItem)
cbMax = cbItem;
if (fEndReached && (pit >= pitStart))
break;
/*如果到区域末尾还没有找到足够大的块,则设置结尾标识
但是在分配额外物理内存之前还不能使用*/
if (!pit->size) {
fEndReached = TRUE;
pitEnd = pitRet;
pit = FIRSTITEM (prgn);
}
}while (!fEndReached || (pit < pitStart));
CarveItem做的是将得到的项细化成所需空间大小的项,并尽可能的向左对齐。下面是其在实际映射空间不足的情况下将空间补齐的程序代码。
if (cbItem < cbSeek) {
pitem pitEnd = (pitem) ((char *) pit + cbItem);
int cbEx;
// 计算需要补齐的页数目
// 注意补齐之后要标记一个新的结尾标识
cbEx = (cbSeek - cbItem + sizeof(item) + PAGE_SIZE-1) & -PAGE_SIZE;
if (!VirtualAlloc ((char *)pitEnd + sizeof(item), cbEx, MEM_COMMIT, PAGE_READWRITE)) {
return NULL; // 内存不足,报错返回
}
//创建一个新的结尾标识
pitNext = (pitem)((char*)pitEnd + cbEx);
pitNext->size = 0;
pitNext->cbTail = pitEnd->cbTail - cbEx;
堆栈的回收
堆栈的回收也有两种情况。如果是直接分配虚拟地址给项,那么只要将该项从pVAitem链接串行上摘出,并将虚拟地址释放。如果是通过区域,那么合并可能出现的相邻可用内存项。
程序代码4.12 堆栈的回收
if (size & 1) { /*虚拟地址映像到项的标记*/
if (php == pit->php) {
pvaitem pva = (pvaitem)((char*)lpMem - sizeof(vaitem));
EnterCriticalSection(&php->cs);
if (pva->pvaBak == NULL) {
DEBUGCHK(php->pvaList == pva || (pvaitem)ZeroPtr(php->pvaList) == pva);
if ((php->pvaList = pva->pvaFwd) != NULL)
php->pvaList->pvaBak = NULL;
} else {
// Item in the middle or end of the list.
DEBUGCHK(php->pvaList != pva);
if ((pva->pvaBak->pvaFwd = pva->pvaFwd) != NULL)
pva->pvaFwd->pvaBak = pva->pvaBak;
} /*找到虚拟地址所在的项,并将其从链接串行上摘出*/
LeaveCriticalSection(&php->cs);
lpMem = (LPVOID)((DWORD)lpMem & -PAGE_SIZE);
VirtualFree(lpMem, size, MEM_DECOMMIT);
VirtualFree(lpMem, 0, MEM_RELEASE);
/*将这段虚拟地址与实际实体地址的映像脱离,仍然保留虚拟地址*/
bRet = TRUE;
}
} else if ((prgn = pit->prgn)->phpOwner == php) {
EnterCriticalSection (&php->cs);
pit->size = -size;
MergeFreeItems (prgn, pit); /*合并可能出现的可用内存项*/
if (prgn->cbMaxFree < -pit->size)
prgn->cbMaxFree = -pit->size;
prgn->cbMaxFree |= REGION_MAX_IS_ESTIMATE;
LeaveCriticalSection (&php->cs);
bRet = TRUE;
}
}
4.3 Windows CE的保护和共享机制
页表的内容
在页表中,每个项目的长度都是32位。事实上,页表中并不需要储存offset的信息,因此Windows CE利用offset的12位作为保护的机制,这也代表Windows CE对内存的保护是以页为单位,并且可利用TestAccess函式作安全性的检查,及VirtualProtect 函式得到或设置一个虚拟地址区域的存取权限。下表4.1是页表保护机制中,各个权限所代表的值及含义:
表4.1 页表保护机制权限设定值
权限 | 值 | 含义 |
PAGE_NOACCESS | 0x01 | 不允许任何类型的存取(读,写,执行)任何试图存取该页的操作都会产生General Protection错误 |
PAGE_READWRITE | 0x02 | 允许读写已提交的页 |
PAGE_READONLY | 0x04 | 只允许读已提交的页 |
PAGE_WRITECOPY | 0x08 | 页可以通过写时拷贝共享数据 |
PAGE_EXECUTE | 0x10 | 允许执行已经提交的页 |
PAGE_EXECUTE_READ | 0x20 | 可以读,执行该页 |
PAGE_EXECUTE_READWRITE | 0x40 | 允许读写执行该页 |
PAGE_EXECUTE_WRITECOPY | 0x80 | 可擦写执行,共享该页。 |
PAGE_GUARD | 0x100 | 设置标记哨,在第一次存取该页的时候产生STATUS_GUARD_PAGE异常,并将该哨清除,通常用于第一次要特殊处理的页的存取中。 |
PAGE_NOCACHE | 0x200 | 强制声明所在实体页不进入cache,重要用于设备驱动程序中。 |
PAGE_PHYSICAL | 0x400 | |
PAGE_WRITECOMBINE | 0x400 |
Windows CE的共享机制
在Windows CE中页的共享在两个层次实现。在逻辑储存层,实现处理程序之间的内存共享,通过建立共享堆栈来实现。在虚拟内存管理的层次,系统通过直接将不同的虚拟地址映像到同一实体地址来实现内存共享,主要实现一些处理程序间通讯的系统呼叫。
堆栈共享
堆栈共享的实现比较简单,只是在系统堆栈中分配一个新的堆栈储存区域建立一个堆栈的数据结构,然后将共享堆栈中的item链接串行穿起来。程序代码4.13为其程序代码。
程序代码4.13 InitSharedHeap函式程序代码
LPBYTE InitSharedHeap(LPBYTE pMem, DWORD size, DWORD reserve)
{
pheap php;
pitem pit;
LPBYTE pbAlloc;
php = (pheap)pMem;
pit = FIRSTITEM(&php->rgn);
if (size) {
InitNewHeap(php, HEAP_IS_SHARED, PAGE_SIZE, size, size);
pbAlloc = HeapAlloc((HANDLE)php, 0, reserve);
//在系统堆栈中为共享堆栈分配储存空间
DEBUGCHK(pbAlloc == (LPBYTE)(pit+1));
//将原有堆栈中的数据结构链直接挂到共享堆栈中。
}
return (LPBYTE)(pit+1);
}
虚拟内存共享
虚拟内存共享的实现要稍微复杂一些,由于要直接读取页表,所以要进入虚拟内存临界区。首先在系统堆栈中分配一块空间建立共享虚拟内存的页表,然后读出页表中的每个实体页数编号,填入目标虚拟地址的页表中。在映像的过程中,要将目标虚拟地址的存取权限加到虚拟地址的页表中。
合法性检查 进入临界区域 源地址是否在安全区 将页目录定义为系统源页目录 扫描整个共享的物理内存区域,将页框号填入目标页表 扫描这个共享的虚拟储存区的页表,将实体页框号拷贝到目标页表 退出临界区 根据位移找到源页目录 是否直接映像物理内存
- WinCE 编程实验(第四章 储存管理)
- WinCE 编程实验(第五章 Windows CE的储存管理)
- WinCE 编程实验(第六章 装置管理)
- WinCE 编程实验(第10 章 虚拟内存)
- WinCE 编程实验(第十六章 项目制作)
- WinCE 编程实验(第一章 引言)
- WinCE 编程实验(内容列表)
- WinCE 编程实验(第二章 Windows CE系统结构)
- WinCE 编程实验(第七章 使用者接口与图形子系统)
- WinCE 编程实验(第11 章 档案系统)
- WinCE 编程实验(第12 章 驱动程序加载机制)
- WinCE 编程实验(第十三章 Windows CE应用程序开发环境)
- WinCE 编程实验(第十四章 Windows CE 驱动程序)
- WinCE 编程实验(第十五章 Windows CE侦错环境)
- 内存管理实验_STM32F1开发指南_第四十二章
- 第四章实验第四题
- 第四章:实验任务
- 第四章实验作业
- 让Java与OpenOffice调情(转)
- 运行程序前执行自己的代码
- SSL连接建立过程分析(4)
- WinCE 编程实验(第三章 进程与线程的描述和调度)
- W39D040A------512 K X 8 CMOS FLASH MEMORY
- WinCE 编程实验(第四章 储存管理)
- 消息钩子函数入门
- WinCE 编程实验(第五章 Windows CE的储存管理)
- 函数调用和异常捕捉的区别
- ProMesh学习笔记-初体验
- 可卑的发明
- WinCE 编程实验(第六章 装置管理)
- 3G 牌照终于发放了
- HTTP协议基础