找回密码
 注册
搜索
查看: 677|回复: 0

[讨论] Windows Mobile 内存管理(四)

[复制链接]
发表于 2009-3-19 10:55:19 | 显示全部楼层 |阅读模式
释放分离堆中的内存
    你可以通过以下调用释放内存块:
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的内存限度







表7-2 Pocket PC的内存限度







    这些内存状态的影响是共享剩余的财富。首先,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章篇幅。
    我们已经看过了程序存储区RAM,这是应用程序可用的RAM。现在是时间,在下两个章节,来看看另一部分RAM,对象存储区。对象存储区支持超过一个文件系统,它也支持一般的注册表API和Windows CE特有的数据库API。
高级模式
B Color Image Link Quote Code Smilies

本版积分规则

Archiver|手机版|小黑屋|52RD我爱研发网 ( 沪ICP备2022007804号-2 )

GMT+8, 2025-2-25 04:06 , Processed in 0.047854 second(s), 16 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表