内存管理
如果你在写Windows CE 程序中遇到的最重要的问题,那一定是内存问题。一个WinCE 系统可能只有4MB 的RAM,这相对于个人电脑来说是十分少的,因为个人电脑的标准配置已经到了128MB 甚至更多。事实上,运行WinCE 的机器的内存十分缺乏,以至于有时候有必要在写程序的时候为节约内存而牺牲程序的整体性能。
幸运的是,尽管WinCE系统的内存很小,但可用来管理内存的函数却十分完善。WinCE实现了Microsoft Windows XP和Microsoft Windows Me中可用到的几乎全部的Win32内存管理API。WinCE支持虚拟内存(virtual memory)分配,本地(local)和分离(separate)的堆(heaps),甚至还有(memory-mapped files)内存映射文件。
像Windows XP一样,WinCE支持一个带有应用程序间内存保护功能的32位平面地址空间,但是WinCE是被设计来应用于不同场合,所以它底层的内存结构不同于Windows XP。这些不同能够影响到你如何设计一个WinCE 应用程序。在这一章中,我将讲述最基础的WinCE内存结构。我也将讲述包括WinCE中可用的内存分配方式中的不同点以及如何使用这些不同的内存类型来最小化你的程序的内存占有率。
内存基础
对所有的电脑来说,系统地运行一个WinCE,需要ROM(只读存储器)和RAM(随机存储器)。但不论如何,在WinCE系统中,ROM和RAM的使用还是稍微有些不同于个人电脑环境。
关于RAM
RAM在WinCE 系统中被分为两个区域:第一个是程序的存储区(program memory),也叫做系统堆(system heap)。第二个是对象存储区(object store)。这个对象存储区有点像一个永久的虚拟RAM磁盘。不同于PC上的旧式的虚拟RAM磁盘,对象存储区保留存储的文件甚至当系统被关闭以后。(脚注)这种安排的原因是WinCE 系统,例如Pocket PC代表性地具有一个主电池和一个备用电池。当用户更换主电池的时候,备用电池的工作是提供电源给RAM以便维持文件在对象存储区的存储。当用户按了重启键之后,WinCE核心就开始寻找在关闭系统前建立的对象存储区,如果找到的话就将继续使用它。
RAM中的另一个区域则用作程序存储区。程序存储区有点像个人电脑中的RAM,它为正在运行的应用程序保存堆和栈的内容。在对象存储区和程序存储区之间的分界线是可以通过移动它来改变的,用户可以在控制面板中找到改变这条分界线的设置。在可用内存降低的(low-memory)条件下,系统将会弹出对话框询问用户是否要将对象存储区RAM划分一些给程序存储区RAM以满足要运行的应用程序的需求。
关于ROM
在个人电脑中,ROM是用来存储BIOS(基本输入输出系统)并且只有64-128KB。在WinCE系统中,ROM大小可以从4MB到32MB并且存放整个操作系统以及和系统捆绑在一起的应用程序。在这种情况下,ROM在WinCE系统中就好像一个只读的硬盘。
在一个WinCE系统中,存储在ROM之上的程序能够以现场执行(Execute in Place,XIP)的方式运行。换句话说,程序可以直接从ROM中执行而不必先加载到RAM中再执行。这种能力对小型系统来说,使之在两个方面具有巨大的优势。代码直接从ROM中执行意味着程序代码不会占据更有价值的RAM。同样,程序在执行前也不必先复制到RAM中,这样就只需要很少的时间来启动一个应用程序。不在ROM中,但是被包含在对象存储区(译者注:上文将对象存储区比作永久的RAM磁盘,故此处要说明,只有Intel力推的nor flash memroy类型才能以XIP方式执行,ROM其实也是一种nor flash memory类型)或闪存卡(Flash memory storage card)中的程序将不能以现场方式执行,它们将被复制到RAM中再执行。
关于虚拟内存
WinCE 实现了系统的虚拟内存管理,在一个虚拟内存系统中,应用程序主要处理这个分离(译者注:物理上可能分离,但系统将它们联系起来),虚拟的地址空间,因此并不涉及到由硬件管理的物理内存。操作系统使用微处理器的内存管理单元来处理虚拟地址和物理地址间的实时转换。
这种虚拟内存方法的优势能从MS-DOS系统复杂的地址空间看出来。一旦请求的RAM超过最初PC设计的640-KB限制,程序设计者将不得不作出像扩展内存一样的计划以便增加可用内存的数量。OS/2 1.x(译者注:IBM研制的操作系统)和Windows 3.0采用了一种基于段(segment-based)的虚拟内存系统来解决问题。应用程序使用虚拟内存不需要知道实际物理内存的位置,只要有内存可用就行。在这些系统中,虚拟内存以一种段的方式被实现了,即可移动的内存块(译者注:段其实就是内存分块)大小从16字节到64KB。64-KB的限制并不是由于段本身原因,而是由于Intel 80286的特性所致,这就是Windows3.x和OS/21.x的分段式虚拟内存系统结构。
分页存储
Intel 80386支持的段大小已经超过64KB,但是Microsoft和IBM开始设计OS/2 2.0,他们选择了一种不同的虚拟内存系统,随后也被386所支持,这就是分页式虚拟内存系统。在一个分页存储的系统中,最小的可被微处理器管理的单元是页(page)。对于Windows NT和OS/2 2.0系统来说,页大小都被设置为386处理器默认的4096字节。当一个应用程序存取一个页的时候,微处理器将转换该页的虚拟内存地址到实际的ROM或RAM中的物理页(译者注:这就是实现了地址映射和转换,将虚拟的和实际的存储单元一一对应),这一页同时被标记以便其他程序对该页的访问将被排斥。操作系统决定虚拟内存页是否有效,如果有效,将做一个物理内存页到虚拟页的映射。
WinCE实现了一个和其他Win32操作系统类似的分页式虚拟内存系统。在WinCE中,一页的大小可以从1024字节到4096字节,基于微处理器的不同而不同。这和Windows XP不同,Windows XP页面尺寸是Intel微处理器所支持的4096字节。对WinCE所支持的CPU类型来说,有486,Intel的Strong-ARM,和Hitachi SH4 都是是用了4096-byte 的页面。NEC 4100在Windows CE 3.0中使用了4-KB的页面尺寸但是在较早期的开放式系统版本中使用了1-KB的页面大小。
虚拟内存页可以处在三种状态:自由(free),保留(reserved),或被提交(committed),)。自由页就像它的名称一样,自由并且可被分配。保留页是虚拟地址已经被保留,并且不能被操作系统或进程中的其他线程重新分配。保留页不能用在别处,但是它同样不能被当前程序使用,因为它没有被映射到物理内存。要想执行映射,它必须被提交,一个提交页能被应用程序保留,并且直接映射到物理地址。
所有我刚才讲述的内容对有经验的Win32 程序员们来说是些陈旧的知识。对Windows CE 程序员来说最重要的东西是学习Windows CE 是如何改变这些因素的。当Windows CE 实现了大部分和它的老大哥Win32一样的内存API集的时候,Windows CE下面的基础结构将影响到上面的程序。在分开来看Window CE 应用程序的内存结构之前,让我们先来看看一些提供系统内存全局状态的函数。
查询系统的内存
如果一个应用程序知道系统当前的内存状态,它将可以较好地管理可用到的资源。WinCE实现了Win32的
GetSystemInfo和
GlobalMemoryStatus函数,
GetSystemInfo函数原型如下:
VOID
GetSystemInfo
(LPSYSTEM_INFO
lpSystemInfo);
它传递了一个指针给SYSTEM_INFO结构,定义如下
typedef struct {
WORD wProcessorArchitecture;
WORD wReserved;
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO;
wProcessorArchitecture
参数表示系统微处理器的架构。它的值是定义在Winnt.h中,例如
PROCESSOR_ARCHITECTURE_INTEL。Windows CE扩展了这些常数,包括
PROCESSOR_ARCHITECTURE_ARM
,PROCESSOR_ARCHITECTURE_SHx
,等等。增加的常数包括像Win32
操作系统支持的网络CPU
(net CPU
)。跳过一些参数,我们看dwProcessorType
参数,它来自于特定的微处理器类型。常数有Hitachi SHx架构中的
PROCESSOR_HITACHI_SH3
和PROCESSOR_HITACHI_SH4
。最后两个参数,wProcessorLevel
和wProcessorRevision
,指明了CPU
类型的特征。wProcessorLevel
参数类似于dwProcessorType
参数,它一个指定的微处理器系列中被定义了,dwProcessorRevision
告诉你模式(model
)和芯片的步进级别(stepping level
)。
dwPageSize
参数说明了微处理器页面的大小,以字节为单位。知道这个值,将会在你直接处理虚拟内存API
的时候带来方便,在此我只作简短说明。lpMinimumApplication瑼ddress
和lpMaximumApplicationAddress
参数说明了应用程序可用到的最小和最大的虚拟内存地址。dwActiveProcessorMask
和dwNumberOfProcessors
参数显示被Window XP
系统支持的多个处理器数量。因为Windows CE
只支持一个处理器,所以你可以忽略这个参数。dwAllocationGranularity
参数说明了一个完整的虚拟内存区域分配的界限。像Windows XP
,Windows CE
规定虚拟区为64-KB
的界限(译者注:作者此处64-KB
的意思是即使你只分配一个字节的内存,系统也将会保留64-KB
的虚拟地址空间给它,这个值一般是由硬件代码实现的,但是不同硬件可能不同值)。
第二个方便的检测系统状态的函数如下:
void
GlobalMemoryStatus(LPMEMORYSTATUS
lpmst);
它返回一个
MEMORYSTATUS
结构,定义为
typedef struct {
DWORD dwLength;
DWORD dwMemoryLoad;
DWORD dwTotalPhys;
DWORD dwAvailPhys;
DWORD dwTotalPageFile;
DWORD dwAvailPageFile;
DWORD dwTotalVirtual;
DWORD dwAvailVirtual;
} MEMORYSTATUS;
dwLength
参数在调用这个函数之前必须初始化。dwMemoryLoad
参数是一个不确定的值;这是一个可用的一般性的参数指示了当前系统的内存使用情况(译者注:该参数是一个近似的百分比值,指明了物理内存的使用情况)。dwTotalPhys
和dwAvailPhys
参数指明了RAM
有多少页被分配给了程序存储区RAM
,和还有多少可用(译者注:实际上是以字节为单位)。这些值不包括分配对象存储区的RAM
。
dwTotalPageFile
和dwAvailPageFile
参数在Windows XP
下和Windows Me
下指明了当前页面文件(paging file
)的状态。因为Windows CE
不支持页面文件,所以这些参数总是0
。dwTotalVirtual
和dwAvailVirtual
参数指明了总共的和可用的可被应用程序存取的虚拟内存页的数量(译者注:参数都是以字节为单位,而不是指页数,dwAvailVirtual
是指未保留和未提交的内存)。
通过GlobalMemoryStatus
返回的信息可以验证Windows CE
内存结构,通过在有32MBRAM
的HP iPaq Pocket PC上调用函数,返回值如下:
dwMemoryLoad 0x18 (24)
dwTotalPhys 0x011ac000 (18,530,304)
dwAvailPhys 0x00B66000 (11,952,128)
dwTotalPageFile 0
dwAvailPageFile 0
dwTotalVirtual 0x02000000 (33,554,432)
dwAvailVirtual 0x01e10000 (31,522,816)
dwTotalPhys
参数表明了系统的32MB RAM
,分配了18.5MB
给程序存储区RAM
,其中12MB
仍然可用。注意这对应用程序来说,并不是通过这次调用,就知道了另外14MB
的RAM
是分配给对象存储区的。要检测分配给对象存储区的RAM
的大小,要使用GetStoreInformation
。
dwTotalPageFile
和dwAvailPageFile
参数是0
,表明页面文件不被Windows CE
所支持。dwTotalVirtual
参数十分有趣,因为它显示了Windows CE
强制给程序的32-MB
的虚拟内存限制。其间,dwAvailVirtual
参数显示了只使用了32MB
虚拟内存的一小部分(译者注:即
33,554,432-31,522,816=2,031,616
)。
应用程序的地址空间
尽管和Windows XP的应用程序设计类似,但Windows CE应用程序地址空间有一个巨大的不同影响到应用程序。在Windows CE之下,一个应用程序被限制在虚拟内存空间中它自己的32MB slot(槽)和 32MB 的slot 1中,slot 1用来加载基于XIP的DLL(译者注:Windows CE将虚拟地址空间分为33个slot,每个slot 32MB,序号从0-32 ,附插图c-1,c-2)。当系统只有4MB RAM的时候,分配给应用程序32MB的虚拟地址空间看起来是比较合理的,Win32的程序员在使用这个2-GB的虚拟地址空间的时候,必须记住对Windows CE应用程序的虚拟地址空间限制。
图7-1展示了一个应用程序的64-MB虚拟地址空间,其中包括32MB用于XIP的DLL空间。
图7-1
Windows CE的内存映射图
要注意的是应用程序是以一个64-KB的内存区域开始从0x10000映射,记得最低的64KB地址空间是由Windows为所有应用程序保留的。文件映象包括代码,静态数据段和资源段。在实际过程中,当应用程序启动时代码页(code pages)不会载入进来,只有在需要该页面被载入的时候,代码才被载入进来。
只读静态数据段(read-only static data segment)和可读写静态数据区(read/write static data areas)只占很少的页面。这些段都是排列在一起的。如同代码一样,只有当这些数据段被应用程序读或者写的时候才会提交给RAM。应用程序的资源将被载入到一些分离的页面中,这些资源是只读的,并且只有当它们被应用程序获取的时候才会分页进入RAM。
应用程序的栈(stack)被映射到资源段之上。栈的段位置很容易被找到,因为它提交的页就在被保留的区域的尾部。栈的表现是从高地址到低地址增长(译者注:即从高到低填满地址)。如果该应用程序有超过一个线程,那么应用程序的地址空间就会保留超过一个的栈的段。
紧接着栈的就是本地堆(local heap)。引导程序保留了大量的页,大约有几百K交给heap来使用,但是只提交满足malloc,new,LocalAlloc函数调用分配的内存(译者注:这里是说,被分配多少内存才可以提交多少内存,没被分配的不能用作提交)。从本地堆的保留页尾部到non-XIP DLL开始的部分剩余保留页面将被映射为自由的保留空间,如果RAM允许,应用程序可以提交这些保留页。Non-XIP DLLs 就是不能被在ROM中现场执行的DLL将被从上至下载入到32MB的地址空间。Non-XIP DLLs 包括那些被压缩存储在ROM中的DLL。被压缩的ROM 中的文件在被载入到RAM中执行前必须先解压缩。
被保留给XIP DLLs的32MB 应用程序地址空间的较高位置。Windows CE 映射这些XIP DLLs的代码进入这个空间(译者注:即较高空间),而可读写段被映射进较低位置。从Windows CE 4.2开始,在ROM中的纯资源的DLL将被载入到应用程序64MB空间之外的虚拟内存空间。
脚注
在PocketPC这样的移动式系统中,当用户按下关闭按钮时系统将不会被真正的关闭,系统进入一种低功耗的挂起状态。