内存分配的不同类型 一个 Windows CE 应用程序有许多不同的内存分配方式。在内存食物链的底端是Virtualxxx 函数,它们直接保留,提交和释放(free)虚拟内存页。接下来的是堆(heap) API。堆是系统为应用程序保留的内存区域。堆有两种风味:当应用程序启动时自动默认分配的本地堆(local heap),以及能够由程序手动创建的分离堆(separate heap)。在堆API之后是静态数据,数据块是被编译器定义好的或者由程序手动创建的。最后,我们来看栈,这是程序为函数存储变量的区域。 一个Windows CE不支持的Win32 内存API是全局堆(global heap)。全局堆API包括Global­Alloc,GlobalFree和GlobalRealloc,将不会出现在 Windows CE中(译者注:很奇怪,我在Windows CE 中仍然可以使用这几个API,并且工作正常,好像Microsoft并没有把它们完全去掉)。全局堆只是从Windows 3.x的Win16 时期继承而来。在Win32中,全部和本地的堆很类似,全局内存一个独特用法是,为剪贴板的数据分配内存,在Windows CE中已经被本地堆替代并加上了句柄。 在Windows CE中最小化内存使用的关键是选择与内存块使用模型相匹配的恰当的内存分配策略。我将回顾一下这些内存类型然后讲述Windows CE应用程序中的最小化内存使用策略。 虚拟内存 虚拟内存是内存类型中最基础的。系统调用虚拟内存API来为其他类型内存分配内存。包括堆和栈。虚拟内存API,包括 VirtualAlloc,VirtualFree和VirtualReSize函数,这些可以直接操作应用程序虚拟内存空间的虚拟内存页面。页面可以保 留,提交给物理内存,或使用这些函数释放。 分配虚拟内存 分配和保留虚拟内存是同过这个函数完成的: LPVOID VirtualAlloc (LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect); VirtualAlloc的第一个参数是要分配内存区域的地址。当你使用VirtualAlloc来提交一块以前保留的内存块的时 候,lpAddress参数可以用来识别以前保留的内存块。如果这个参数是NULL,系统将会决定分配内存区域的位置,并且围绕64-KB的范围(译者 注:就是前面说提及的最小内存分配尺寸)。第二个参数是dwSize,要分配或者保留的区域的大小。这个参数以字节为单位,而不是页,系统会根据这个大小 一直分配到下页的边界。 flAllocationType参数指定了分配的类型,你可以指定或者合并以下标 志:MEM_COMMIT,MEM_AUTO_COMMIT,MEM_RESERVE和MEM_TOP_DOWN。MEM_COMMIT标志分配程序使用 的内存,MEM_RESERVE保留虚拟地址空间以便以后提交。保留的页不能存取直到调用VirtualAlloc的时候再次指定了MEM_COMMIT 标志。第三个标志,MEM_TOP_DOWN,告 诉系统从最高可允许的虚拟地址开始映射应用程序。 The MEM_AUTO_COMMIT标志是唯一一个Windows CE最方便的标志,当这个参数被指定了之后,内存块立即被保留,当其中的页被第一次存取的时候,系统将自动提交该页。这允许你分配大块的虚拟内存而不需要 顾及系统和实际RAM分配直到当前页被第一次使用。自动提交内存的缺点是,物理RAM需要退回当页面被第一次访问时可能不可用的页面。在这种情形下,系统 将产生一个异常(exception)(译者注:可能会出现因为无法访问而出错)。 VirtualAlloc可以通过并行多次调用提交 一个区域的部分或全部来保留一个大的内存区域。多重调用提交同一块区域不会引起失败。这使得一个应用程序保留内存后可以随意提交将被写的页。当这种方式不 在有效的时候,它会释放应用程序通过检测被保留页的状态看它是否在提交调用之前已经被提交。 flProtect参数指定了被分配区域的访问保护方式。这些不同的标志被总结在下面的列表中: PAGE_READONLY 该区域为只读。如果应用程序试图访问区域中的页的时候,将会被拒绝访问。 PAGE_READWRITE 区域可被应用程序读写。 PAGE_EXECUTE 区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。 PAGE_EXECUTE_READ 区域包含可执行代码,应用程序可以读该区域。 PAGE_EXECUTE_READWRITE 区域包含可执行代码,应用程序可以读写该区域。 PAGE_GUARD 区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限。 PAGE_NOAccess 任何访问该区域的操作将被拒绝。 PAGE_NOCACHE RAM中的页映射到该区域时将不会被微处理器缓存(cached)。 PAGE_GUARD和PAGE_NOCHACHE标志可以和其他标志合并使用以进一步指定页的特征。PAGE_GUARD标志指定了一个防护页 (guard page),即当一个页被提交时会因第一次被访问而产生一个one-shot异常,接着取得指定的访问权限。PAGE_NOCACHE防止当它映射到虚拟 页的时候被微处理器缓存。这个标志方便设备驱动使用直接内存访问方式(DMA)来共享内存块。 区域和页 在我继续谈论虚拟内存API之前,我需要说明一个比较细微的差异。虚拟内存在区域内被保留是以64KB为基础的。在区域内的页面能够一页一页地被提交(译者注:前面说到在Windows CE中每页是4096字节或1024字节)。你可以直接提交一页或 者几页而不是保留区域的全部页。但是对页或几页来说,直接提交的仍是以64-KB为单位(译者注:可以直到被提交的页数量足够填满64KB才真正提交), 因为这个原因,最好保留一块64-KB的虚拟内存,然后提交那些需要的页到区域里。 因为对每个进程32MB虚拟内存地址空间的限制,这就有了一个最大值 32MB/64KB-1=511,这是虚拟内存在内存溢出前能被保留的最大值。接下来,有个例子,代码段如下: #define PAGESIZE 1024 // Assume we're on a 1-KB page machine for (i = 0; i < 512; i++) pMem[i] = VirtualAlloc (NULL, PAGESIZE, MEM_RESERVE │ MEM_COMMIT,PAGE_READWRITE); 代码分配512个单页的虚拟内存。甚至你系统还有一半的可用RAM,VirtualAlloc也会在完成分配前失败。因为它的运行已经超出了应用程序的 虚拟地址空间。发生这种情况是因为每1-KB的块要占用64-KB的空间,接下来应用程序的代码,栈,和本地堆也要映射到同样的32-MB虚拟地址空间, 可用的虚拟分配区域通常不超过475个。 一个比较好的分配512块特殊内存的方法是这样做: #define PAGESIZE 1024 // Assume we're on a 1-KB page machine. // Reserve a region first. pMemBase = VirtualAlloc (NULL, PAGESIZE * 512, MEM_RESERVE, PAGE_NOAccess); for (i = 0; i < 512; i++) pMem[i] = VirtualAlloc (pMemBase + (i*PAGESIZE), PAGESIZE, MEM_COMMIT, PAGE_READWRITE); 代码首先保留了一块区域,页面将在以后被提交。因为区域已经被先保留了,提交页就不受64-KB限制(译者注:只有保留页最小值受64KB限制),等等,如果你系统中有512KB的可用内存,分配将会成功。 尽管我刚才给你看的是一个人为的例子(还有比直接分配虚拟内存更好的方法来分配1-KB的内存块),这中内存分配方法验证了一个重要的不同(对于其他 Windows系统)。在桌面版本的Windows中,工作中的应用程序有一个完全的2-GB的虚拟地址空间。在Windows CE中,一个程序员必须明白每个应用程序只被保留了较小的32-MB虚拟地址空间。 释放虚拟内存 你可以通过调用VirtualFree来取消提交,或释放虚拟内存。从物理RAM页中取消提交或者取消映射,但是保持页被保留的状态。函数原型如下: BOOL VirtualFree (LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType); lpAddress参数是一个指针,指向要被释放或取消提交的虚拟内存的区域。dwSize参数指明要取消提交区域的大小,以字节为单位。如果区域要被 释放,这个值必须是0,dwFreeType参数包含了操作类型标志,MEM_DECOMMIT标志指定了区域将被取消提交但是仍被保 留,MEM_RELEASE标志说明区域要取消提交并且释放。 在区域中的所有的页通过VirtualFree被释放必须处在同样的情况下。更确切地说,区域中的全部页要被释放,那这些页要么都是被提交的页,要么都是被保留的页。如果有些页被提交,有些页被保留,那么VirtualFree函数调用就会失败。 改变和查询权限 你可以通过调用VirtualProtect来修改最初通过VirtualAlloc指定的虚拟内存区域的访问权限。这个函数只能改变被提交的页的访问权限。函数的原型如下: BOOL VirtualProtect (LPVOID lpAddress, DWORD dwSize, DWORD flNewProtect, PDWORD lpflOldProtect); 开始的两个参数lpAddress和dwSize,指定了函数作用的块的大小。flNewProtect参数包含区域的新的保护标志。这些标志和我前面 提到的VirtualAlloc函数使用的一样。lpflOldProtect参数指向一个DWORD,将返回旧的保护标志(译者注:如果此处为NULL 或指向一个无效的变量,函数将会失败)。 当前区域的保护权限可用通过下面的调用查询: DWORD VirtualQuery (LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lPBuffer, DWORD dwLength); lpAddress参数包含区域开始查询的地址。lPBuffer指针指向我很快就要提到的一个PMEMORY_BASIC_INFORMATION结构。第三个参数dwLength,必须包含PMEMORY_BASIC_INFORMATION结构的大小。 PMEMORY_BASIC_INFORMATION结构被定义如下: typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; DWORD AllocationProtect; DWORD RegionSize; DWORD State; DWORD Protect; DWORD Type; } MEMORY_BASIC_INFORMATION; MEMORY_BASIC_INFORMATION结构的第一个字段是BaseAddress,是传递给VirtualQuery函数的一个地址。 AllocationBase字段包含使用VirtualAlloc函数分配的区域的基地址,AllocationProtect字段包含区域原来被分配 时的保护属性。RegionSize字段包含从传递给VirtualQuery的指针开始到一系列具有相同属性的页为结尾的区域大小(译者注:这里是从基 地址开始)。State字段包含区域中页的状态-自由,保留,提交。Protect字段可以包含MEM_PRIVATE标志,指明该区域包含应用程序私有 的数据;MEM_MAPPED指明该区域被映射为一个内存映射文件;MEM_IMAGE指明该区域被映射为一个EXE或DLL模块。 理 解VirtualQuery最好的方式是看例子,比方说一个应用程序保留了16,384字节(在以页面大小为1-KB的机器中占16页)。系统从地址 0xA0000开始保留这16-KB的块。后来应用程序从最初的区域中提交了从第2048字节(2页)开始的9216字节(9页)。 如果一个对VirtualQuery的调用中,lpAddress指向第四页的区域(地址0xA1000),返回值如下: BaseAddress 0xA1000 AllocationBase 0xA0000 AllocationProtect PAGE_NOACCESS RegionSize 0x1C00 (7,168 bytes or 7 pages) State MEM_COMMIT Protect PAGE_READWRITE Type MEM_PRIVATE BaseAddress字段包含传递给VirtualQuery的地址,值为0xA1000,在最初的区域中是第4096字节。 AllocationBase字段包含最初区域的地址。当AllocationProtect设为PAGE_NOACCESS时,指明区域是最初被保留 的,而不是直接提交。RegionSize字段包含传递给VirtualQuery的指针0xA1000开始,到被提交的页结束地址0xA2C00的字节 数。State和Protect字段包含的标志表明当前的页状态。Type字节表明区域被应用程序分配给自己使用。 堆 很明显,以页为单位分配内存对应用程序来说效率是很低的。为了优化内存的使用,应用程序需要以字节为单位分配和释放内存,或者至少以每8字节为单位。系统通过堆来实现这种分配方式。使用堆可以免去处理由Windows CE支持的不同微处理器的不同页面大小。一个应用程序可以简单地在堆中分配一块内存,由系统来处理分配需要的页数。 就像我前面提到的,堆是系统为应用程序保留的虚拟内存区域。系统提供大量的函数来在堆中分配和释放内存块,并且间隔比页要小(译者注:例如每页大小为 4KB,而堆分配可以字节为单位)。当内存由应用程序的堆分配时,系统自动分配调整堆大小来满足需要,当堆中的内存块被释放时,系统会查看是否整页被释 放,如果是的话,那么该页将被回收。 不同于Windows XP,Windows CE只支持在堆中分配固定(fixed)的块。这简化了内存块在堆中的处理,但是这使得堆在分配和释放一段时间后会产生碎片。当堆里已经清空的时候,仍然 会占用大量的虚拟内存页,因为系统不能在堆中内存页没有完全释放的时候回收这些页(译者注:因为堆以字节为单位,一页中可能有的块需要被释放,其他的块不 需要,所以整页都不会被释放)。 当应用程序启动的时候,每个程序都会有一个由系统创建的默认或本地堆。本地堆中的内存块,可以通过 LocalAlloc,LocalFree和LocalRealloc来分配,释放和改变大小。一个应用程序也可以建立分离堆。这些堆和本地堆有着相同的 属性,但是是通过一组Heapxxxx函数来管理的。 本地堆 在默认情况下,Windows CE最初会保留192,512字节给本地堆,但是只提交被分配的页。如果应用程序在本地堆中分配了超过188KB,系统将会分配更多的空间给本地堆。增加 堆大小将需要一个分离的,不连续的保留地址空间作为堆的附加空间。应用程序不应该假设本地堆被包含在一块虚拟地址空间里。因为Windows CE 的堆只支持固定的块,Windows CE执行的只是Win32本地堆函数的子集,提供必要的分配,改变大小,释放固定的本地堆内存块。 在本地堆中分配内存 你可以通过一下调用在本地堆中分配一块内存: HLOCAL LocalAlloc (UINT uFlags, UINT uBytes); 调用返回一个HLOCAL,这是本地内存块的句柄,但是由于内存块是固定分配的,所以返回值可以被简单地看作是一个指向块的指针。 uFlags参数描述了内存块的特征。标志由于Windows CE被限制固定分配操作,只支持以下内存: LMEM_FIXED 在本地堆中分配一个固定内存块,因为本地堆分配已经固定,所以是多余的。 LMEM_ZEROINIT 初始化内存内容为0。 LPTR 合并LMEM_FIXED和LMEM_ZEROINIT标志。 uBytes参数指定了要分配的内存块的大小,以字节为单位。块大小要补齐,但是只针对后面8字节范围。 释放本地堆的内存 你可以通过以下调用释放内存块: HLOCAL LocalFree (HLOCAL hMem); 函数需要本地堆内存句柄,成功会返回NULL。如果调用失败,会返回内存块的句柄。 改变和查询本地堆内存的大小 你可以通过调用改变本地堆的分配: HLOCAL LocalReAlloc (HLOCAL hMem, UINT uBytes, UINT uFlag); hMem参数是一个由LocalAlloc返回的指针(句柄)。uBytes参数是内存块的新大小。uFlag参数包含给新内存块的标志。在 Windows CE中,有两个新标志与之相关,LMEM_ZEROINIT和LMEM_MOVEABLE。LMEM_ZEROINIT表示调用函数后内存块中新增加的区 域被初始化为0。LMEM_MOVEABLE标志告诉Windows,当内存块增加后,没有合适的空间容纳内存块时,函数可以立即移动内存块。如果没有这 个标志,当你没有合适的空间来满足需要的时候,LocalRealloc将会出现out-of-memory的错误而失败,如果你指定了 LMEM_MOVEABLE标志,调用将会返回句柄(实际是指向内存块的指针)。 内存块的大小可以通过以下调用查询: UINT LocalSize (HLOCAL hMem); 返回内存块最少需要的内存大小。像我前面提到的,Windows CE本地堆自动以8个字节来补齐(译者注:就是分配1字节要占8字节)。 分离堆 为了避免本地堆的碎片,并且如果你要分配连续的内存块,较好的办法是建立分离堆,但将花费一定的时间。一个例子就是,文本编辑器为要编辑的文件建立多个分离堆。当文件被打开或者关闭的时候,堆随之建立和销毁。 在Windows CE下的堆和Windows XP下有着同样的API。唯一值得注意的不同是缺少HEAP_GENERATE- _EXCEPTIONS标志。在Windows XP下,该标志表示系统在分配请求不合适的时候产生一个异常。 建立一个分离堆 你可以通过以下调用建立一个分离堆。 HANDLE HeapCreate (DWORD flOptions, DWORD dwInitialSize, DWORD dwMaximumSize); 在Windows CE中,第一个参数flOptions必须为空或包含HEAP_NO_SERIALIZE标志。默认情况下,Windows堆管理程序防止一个进程中的两 个线程在同意时间访问堆。这个串行参数防止系统用来跟踪堆中内存块分配的堆指针被破坏。在其他版本的Windows中,当你不需要这种保护时可以使用 HEAP_NO_SERIALIZE标志。在Windows CE中,该标志是为了兼容性而提供的,所有的堆访问都是串行的(译者注:串行即非并行,只能依次访问)。 其他两个参数,dwInitialSize和dwMaximumSize,指定了最初的大小和预期的堆最大值。dwMaximumSize的值确定虚拟 内存空间保留给堆多少页。如果你想让Windows来决定有多少页可以保留,你可以把这个参数设为0。默认一个堆的大小是 188KB,dwInitialSize参数决定了有多少这些保留的页将被提交。如果该参数为0,表示堆将一页一页提交。 在分离堆中分配内存 你可以通过以下调用分配内存 LPVOID HeapAlloc (HANDLE hHeap, DWORD dwFlags, DWORD dwBytes); 注意,返回值是一个指针,而不是和LocalAlloc函数一样的句柄。分离堆总是分配固定的内存块,甚至在Windows XP和Windows Me中也是一样。第一个参数是通过HeapCreate调用返回的句柄。dwFlags参数可以是两个自说明的(self-explanatory)标志 之一HEAP_NO_SERIALIZE和 HEAP_ZERO_MEMORY。最后一个参数dwBytes指定了要分配的内存块字节数。大小要和DWORD补齐。 释放分离堆中的内存 你可以通过以下调用释放内存块: BOOL HeapFree (HANDLE hHeap, DWORD dwFlags, LPVOID lpMem); dwFlags参数唯一的标志是HEAP_NO_SERIALIZE,当hHeap包含堆句柄时,lpMem参数指向要释放的内存块。 改变和查询分离堆中内存的大小: 你可以通过以下调用改变堆大小。 LPVOID HeapReAlloc (HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, DWORD dwBytes); dwFlags参数包含三种标志的组合:HEAP_NO_SERIALIZE,HEAP_REALLOC_IN_PLACE_ONLY和 HEAP_ZERO_ MEMORY。其中较新的标志是HEAP_REALLOC_IN_PLACE_ONLY,这个参数告诉堆的管理者,找不到要分配的块的空间,重分配操作失 败。这个标志方便的地方在于当你有了一些指向内存数据块的指针,并且你不想改变内存块。lpMem参数是一个指向要改变大小的内存块的指 针,dwBytes参数是被请求的新内存块的大小。注意,HeapReAlloc中HEAP_REALLOC_IN_PLACE_ONLY标志提供和 LocalReAlloc中LMEM_MOVEABLE相反的作用。HEAP_REALLOC_IN_PLACE_ONLY防止在分离堆中对内存块默认的 移动操作。而LMEM_MOVEABLE允许本地堆中对内存块的默认移动操作。如果HeapReAlloc成功,就返回一个指向内存块的指针,否则就返回 NULL。除非你指定内存块不可重新定位,那么当内存块因为堆中空间不足时将不得不重定位,因此造成返回指针的值将与原来不同。 要决定实际的内存块大小,你可以作以下调用: DWORD HeapSize (HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem); 参数就像你想象的:有堆的句柄,单选标志HEAP_NO_SERIALIZE,和指向内存块的指针。 销毁一个分离堆 你可以通过以下调用完全释放一个堆: BOOL HeapDestroy (HANDLE hHeap); 在堆中单个的内存块并不需要在销毁堆前释放。 最后一个是写DLL时比较有价值的函数: HANDLE GetProcessHeap (VOID); 返回的是调用DLL时进程的本地堆的句柄。这个函数允许一个DLL在调用者进程的本地堆中分配内存。GetProcessHeap返回的句柄可以供其他堆调用使用,HeapDestroy除外。 栈 栈是Windows CE内存类型中最容易使用的(自行管理)。在Windows CE中的栈像其它操作系统一样,是被引用函数的临时变量存储区。操作系统也用栈来存储函数的返回地址和在异常处理中微处理器寄存器的状态。 在系统中,Windows CE给每个线程一个分离的栈。默认情况下,系统中每个栈大小最大被限制为58KB。在一个进程中,每个分离的线程可以增加栈的大小直到58-KB的限制。 这个限制使得要我们要知道Windows CE如何对栈管理。当线程被建立的时候,Windows CE保留一个64-KB的区域给每个线程的栈。栈增加时,提交虚拟内存页是从上至下的。当栈减小时,系统将处于的低内存环境(low-memory),会 回收在栈下面未使用但是仍然被提交的页。58KB的限制来源于64-KB的区域减去用来防止栈的上溢和下溢的页面数量。 当一个应用程序 建立一个新的线程时,栈的最大尺寸可以通过建立线程时CreateThread调用来指定。应用程序的主线程的栈大小可以通过应用程序被连接时的连接器开 关(linker switch)来指定。同样会有一些页用作防护,但是栈的大小可以指定至1MB。注意,这个指定大小同样会被用作所有分离线程栈的默认栈大小。那就是说, 如果你指定主栈为128KB,程序中所有其他的线程栈大小也限制为128KB,除非在用CreateThread建立线程时指定一个不同的大小。 当你计划如何在应用程序中使用栈的时候,另一个要值得考虑事情的是。当应用程序调用一个需要栈空间的函数时,Windows CE会试图立即提交满足要求的当前栈之下的页面,如果没有物理RAM可用,需要栈空间的线程将会暂时停止。如果请求在短时间内得不到允许,可能产生一个异 常。但是如果系统不发生异常的化,Windows CE将会最大限度释放请求的页。我将简短地说明一下低内存环境,但现在你只需要记住在的内存环境中不要尝试使用大量的栈空间。 静态数据 C和C++应用程序有一个预先定义好的内存块,这是由应用程序被装载时自动分配的。这些块被用来存储静态分配的字符串,缓冲区和全局变量,同时也包括通过静态连接到应用程序的静态库函数中的缓冲区。这些对C程序员来说都不陌生,但是在Windows CE下,这是最后一块可以在RAM之外压缩的空间(译者注:作者的意图是尽可能压缩内存占有率)。 Windows CE分配给应用程序两块RAM中的内存块存放静态数据,一个是可读写数据(read/write data)和只读数据(read only data)。因为这些区域是基于页分配的,所以你可以在一页的静态数据开始到下一页开始之间找到一些剩余空间。细微调整Windows CE应用程序就是要写满这些剩余的空间。如果你在静态数据区有空间,最好把一个或两个缓冲区放到静态数据区,避免动态分配缓冲区。 另一 个值得考虑的事情是你是否在写一个基于ROM的应用程序。你要把尽可能多的数据移到只读静态数据区。Windows CE不会分配只读的RAM给基于ROM的应用程序。并且,ROM页会直接映射到虚拟地址空间。这实际上就给你了一个无限制的只读空间,而且不会影响到应用 程序对RAM的需求。 确定静态数据区大小的方法是查看连接器产生的映象(map)文件。映象文件主要用于调试(debug)目的来确定 函数和数据的位置。但是如果你知道查看什么地方的话,它也可以用来显示静态数据的大小。列表7-1显示了一个由Visual C++产生的示例映象文件的一部分。 列表7-1。映象文件的顶部显示了应用程序数据段的大小 memtest Timestamp is 34ce4088 (Tue Jan 27 12:16:08 1998) Preferred load address is 00010000 Start Length Name Class 0001:00000000 00006100H .text CODE 0002:00000000 00000310H .rdata DATA 0002:00000310 00000014H .xdata DATA 0002:00000324 00000028H .idata$2 DATA 0002:0000034c 00000014H .idata$3 DATA 0002:00000360 000000f4H .idata$4 DATA 0002:00000454 000003eeH .idata$6 DATA 0002:00000842 00000000H .edata DATA 0003:00000000 000000f4H .idata$5 DATA 0003:000000f4 00000004H .CRT$XCA DATA 0003:000000f8 00000004H .CRT$XCZ DATA 0003:000000fc 00000004H .CRT$XIA DATA 0003:00000100 00000004H .CRT$XIZ DATA 0003:00000104 00000004H .CRT$XPA DATA 0003:00000108 00000004H .CRT$XPZ DATA 0003:0000010c 00000004H .CRT$XTA DATA 0003:00000110 00000004H .CRT$XTZ DATA 0003:00000114 000011e8H .data DATA 0003:000012fc 0000108cH .bss DATA 0004:00000000 000003e8H .pdata DATA 0005:00000000 000000f0H .rsrc$01 DATA 0005:000000f0 00000334H .rsrc$02 DATA Address Publics by Value Rva+Base Lib:Object 0001:00000000 _WinMain 00011000 f memtest.obj 0001:0000007c _InitApp 0001107c f memtest.obj 0001:000000d4 _InitInstance 000110d4 f memtest.obj 0001:00000164 _TermInstance 00011164 f memtest.obj 0001:00000248 _MainWndProc 00011248 f memtest.obj 0001:000002b0 _GetFixedEquiv 000112b0 f memtest.obj 0001:00000350 _DoCreateMain 00011350 f memtest.obj. 在列表7-1中的映象文件指出了EXE文件有五个区。区0001是文本段,包含程序中可执行的代码。区0002包含只读(read-only)静态数 据。区0003包含可读写(read/write)静态数据。区0004包含调用其他DLL的固定表。最后,区0005是资源区,包含应用程序的资源,例 如菜单和对话框模板。 让我们来看看.data,.bss和.rdata行。.data区包含已初始化的可读写数据。如果你这样初始化了一个全局变量: static HINST g_hLoadlib = NULL; g_loadlib变量将结束在.data段末尾。.bss段包含未初始化的可读写数据。一个缓冲被定义如下: static BYTE g_ucItems[256]; 以.bss段为结尾。最后一个段.rdata,包含只读数据。你使用const关键字定义的静态数据结束在.rdata段。有一个结构的例子,使我用来作消息查询表的: // Message dispatch table for MainWindowProc const struct decodeUINT MainMessages[] = { WM_CREATE, DoCreateMain, WM_SIZE, DoSizeMain, WM_COMMAND, DoCommandMain, WM_DESTROY, DoDestroyMain, }; .data和.bss块被折叠进0003区,如果你将第三区的所有块大小加起来,总共为 0x2274,或8820字节。为和下页对齐,读写数据区将占9页,那么就有396字节未使用(译者注:1024*9-8820=396)。因此在这个例 子中,把一个或者两个缓冲区放入静态数据区比较合适。只读数据段0002区,包括.rdata,占0x0842或2114字节,占3页,剩余958字节, 几乎是一整页。在这种情况下,移动75字节的常量数据从只读段到可读写段将在应用程序加载时节约一页的RAM。 字符串资源 有一个经常忘记的只读区域时应用程序的资源段,像我前面在第四章提到的Windows CE的新特性有一个LoadString函数,值得再次重复。如果你调用LoadString时指向缓冲区的指针写0,函数将返回一个指向资源段中字符串的指针。例子如下: LPCTSTR pString; pString = (LPCTSTR)LoadString (hInst, ID_STRING, NULL, 0) 返回的字符串是只读的,但是它允许你应用字符串而不需要分配一个缓冲给字符串。这里警告一下,字符串不能以0结尾,除非你在资源编译器命令行中加了-n开关。不管如何,单词必须是先于字符串资源长度(译者注:作者此处意思可能是说长度包含字符串资源的长度)。 选择适当的内存类型 现在我们已经看过了不同类型的内存,是时候来考虑最好的使用办法了。对大的内存块来说,直接分配虚拟内存是最好的办法,一个应用程序可以保留很多的地址 空间(直到应用程序32MB的限制)但是只能在一个时间提交必须的页。直接分配虚拟内存是最灵活的内存分配方式,它把页间隔(granularity)的 负担以及对保留页和提交页都交由我们负担。 本地堆是很方便的,它不需要创建并且会自动随着需求扩大。但碎片是这里的问题。但是要考虑到 Pocket PC的应用程序可能会运行几星期或几个月的时间。在Pocket PC上没有关闭电源的按钮,只有挂起命令。因此,你考虑内存碎片的时候不要假设用户会打开应用程序,改变一个项目,然后关闭它。用户可能打开程序然后让它 一直运行以至于程序就像一个快捷方式(quick click away)。 分离堆的优点是当你不用时可以销毁,把碎片消灭在萌芽状态。有一点不好的就是分离堆需要手动创建和销毁。 静态数据区是放置一两个缓冲区的好地方,因为页面是已经被分配的。管理静态数据的关键是使静态数据段大小尽可能地接近,但是要超过你目标处理器的页面的大小。当常量数据在只读段中,往往较好的办法是把它移到可读写段中。但当应用程序被烧到ROM中时,你不要这么做。常量数据越多会比较好,因为它不占RAM。只读段方便应用程序从对象存储区启动,因为只读页能通过操作系统丢弃和重载。 栈用起来比较简单而且到处存在。唯一要考虑的是栈的最大尺寸和在的内存环境下扩大栈的问题。确定你的应用程序在关闭的时候不需要大量栈空间。当程序被关闭时,如果系统挂起你程序中的一个线程,用户可能会丢失数据。这会使顾客不满意。 低内存环境 当系统运行在一个低RAM环境中,应用程序将调整并最小化它们的内存使用。Windows CE运行在一个几乎永久的低内存环境中。Pocket PC被特意设计为运行低内存环境。在Pocket PC中的应用程序没有关闭按钮,当系统需要更多内存时,外壳(shell)自动关闭这些程序。正因为如此,Windows CE有许多方法来管理运行在低内存系统中的程序。 WM_Hibernate 消息 Windows CE第一个最明显的变化时是增加了WM_Hibernate消息。Windows CE的shell发送消息给最顶层的有WS_OVERLAPPED式样(那就是说,既没有WS_POPUP也没有WS_CHILD式样)和 WS_VISIBLE式样的窗口。这些限制将允许大多数程序至少有一个窗口可以接受WM_HIBERNATE消息。有一个例外就是,当应用程序不能真正结 束程序而只是简单隐藏所有窗口。这种方式允许应用程序可以快速启动,因为它下次只是显示窗口。但是这就意味着,当用户想关闭它们的时候仍然占据着RAM。 这对程序设计来说是正确的,但是不应用在Windows CE中,这种方式会造成程序被隐藏时总处在冬眠(hibernate)模式,因为它们永远接收不到WM_HIBERNATE消息。 Shell发送WM_HIBERNATE消息给最顶层的窗口在Z轴相反的位置(reverse Z-order)直到内存被释放,使可用内存超过系统预先的限制。当应用程序接收到一个WM_HIBERNATE消息,它会尽可能减少内存占有程度。这包 括释放被缓冲(cached)的数据;释放GDI对象,例如字体,位图和画刷;并销毁任何窗口控件。从本质上来说,应用程序将会减少内存到维持它内部状态 的最小值。 如果发送WM_HIBERNATE消息给后台的应用程序不能释放足够的内存以便使系统离开内存被限制的状态。WM_HIBERNATE消息将会发送给前台程序。如果你正在冬眠的程序开始销毁窗口的控件,你必须确保它不是前台的程序,控件消失不会给用户带来兴奋的感觉而是困惑。 内存限度 Windows CE监视系统自由的RAM,并对越来越少的RAM作出响应。当很少内存可用时,Windows CE首先发送WM_HIBERNATE消息,接下来会限制可能的内存分配。下面的两个表显示了Explorer shell和Pocket PC引发的低内存事件的自由内存级别。Windows CE定义了是个内存状态:normal,limited,low和critical。系统的内存状态依赖于整个系统有多少内存可用。这些限制都比4-KB 页要高,因为系统具有内存最小分配限制,就像7-1和7-2的表。 表7-1 Explorer Shell的内存限度 事件 自由内存 1024-Page Size 自由内存 4096-Page Size 注解 Limited-memory state 128 KB 160 KB 发送 WM_HIBERNATE 消息给in reverse Z-order的应用程序。释放栈空间并回收利用。 Low-memory state 64 KB 96 KB 限制虚拟内存分配为16 KB。 显示Low-memory对话框。 Critical-memory state 16 KB 48 KB 限制虚拟内存分配为8KB。 表7-2 Pocket PC的内存限度 事件 自由内存 1024-Page Size 自由内存 4096-Page Size 注解 Hibernate threshold 200 KB 224 KB 发送 WM_HIBERNATE 消息给in reverse Z-order的应用程序。 Limited-memory state 128 KB 160 KB 开始关闭在 reverse Z-order上的应用程序。释放栈空间并回收利用。 Low-memory state 64 KB 96 KB 限制虚拟内存分配为16 KB。 Critical-memory state 16 KB 48 KB 限制虚拟内存分配为8 KB。 这些内存状态的影响是共享剩余的财富。首先,WM_HIBERNATE消息被发送给应用程序,并请求减少它们的内存占有率,当应用程序被发送了一个 WM_HIBERNATE消息后,系统将检测内存级别,确认是否可用内存在限度之上,如果可用内存不足,WM_HIBERNATE消息将被发送给下一个程 序。这会持续到所有程序被发送了WM_HIBERNATE消息。 Exlporer shell和Pocket PC的低内存策略在这点上有区别。如果Explorer shell运行时,系统会显示OOM(out of memory)对话框,并请用户确认是否关闭一个应用程序或把对象存储区的RAM重新划分给程序内存。如果用户选择了其中之一,仍然没有足够的内 存,out of memory对话框将会再次出现,这个过程会重复,直到H/PC有足够的在限度之上的内存。 对Pocket PC来说,操作稍微有些不同。Pocket PC shell自动开始关闭最近最少使用的应用程序,而不询问用户。如果关闭除了前台程序和shell之外的所有程序,仍然没有足够内存,系统将会使用其他的技术来从栈开始清理自由的页,并限制虚拟内存分配。 如果在任何一个系统上,应用程序被请求关闭却没有关闭,系统在8秒钟后将会清理该应用程序。这就是一个应用程序不要分配大量的栈空间的原因。如果应用程 序被关闭而导致低内存环境,很可能是栈空间不能分配,应用程序将被挂起。如果发生在系统请求应用程序关闭以后,可能是清除内存以后没有适当的恢复状态。 在low和critical-memory状态,应用程序被限制了内存分配的大小。在这些情况下,甚至还有可以满足要求的内存剩余情况下,请求分配大过 允许限度的虚拟内存将会被拒绝。记住,并不止是虚拟内存分配被限制,堆分配和栈分配也被禁止,要满足分配请求,那么分配时需要虚拟内存在可允许的限制之 上。 我这里要指出,发送WM_HIBERNATE消息和自动关闭应用程序是由系统的shell执行的。在一个OEM自己可以编写 shell的嵌入式系统中,实现WM_HIBERNATE消息和其他内存管理技术是OEM厂商的责任。幸运的是,Microsoft Windows CE PlatForm Builder提供了Exlporer shell实现WM_HIBERNATE消息的源码。 这里不言而喻,应用程序要检查任何内存分配调用的返回代码,但是因为这里还没说,所以我还是要说。检查内存分配调用的返回代码。在Windows CE中比在桌面版本的Windows中可能有更多的机会导致内存分配失败。应用程序必须很好地实现拒绝内存分配。 Windows CE不支持完全的Win32内存管理API,但是很清楚这里有对WindowsCE设备受限制内存的足够支持。一个极好的学习Win32错综复杂的内存管 理API来源是Jeff Richter’s Programming Applications for Microsoft Windows (Microsoft Press, 1999)。当Jeff和我总结上述相同问题的时候,他在内存管理上花了6章篇幅。 来源:http://www.winbile.net/cms/News/Newsc7c69i8334.aspx