内存分配的不同类型
一个Windows CE 应用程序有许多不同的内存分配方式。在内存食物链的底端是
Virtualxxx
函数,它们直接保留,提交和释放(free
)虚拟内存页。接下来的是堆(heap
) API
。堆是系统为应用程序保留的内存区域。堆有两种风味:当应用程序启动时自动默认分配的本地堆(local heap
),以及能够由程序手动创建的分离堆(separate heap
)。在堆API
之后是静态数据,数据块是被编译器定义好的或者由程序手动创建的。最后,我们来看栈,这是程序为函数存储变量的区域。
一个Windows CE
不支持的Win32
内存API
是全局堆(global heap
)。全局堆API
包括Global瑼lloc
,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页)。图7-2显示了这个假设的情况。
图7-2被保留的区域有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
字节表明区域被应用程序分配给自己使用。