发信人: skyice()
整理人: skyice(2000-06-22 00:26:13), 站内信件
|
进程私有内存
尽管你可能一直坚持用 new 作为内存分配器,但它并不总是最好的选
择。例如,对于大型对象,使用原始 Win32 内存分配器可以得到更多的控
制权。对于性能很关键或者实时性很强的应用程序,你可能会决定创建单类
分配器。在《The Designed and Evolution of C++》一书的第 10 章中,
Bjarne Stroustrup 推荐对频繁使用的类采用这种方法。
即使操作符 new 是你的主要分配器,你也可能会想把结果指针存储在
不是常规变量的某个地方。特别是,Win32 API 提供内存包(memory wa-
lLets,指窗口额外字节和线程局部存储)来隐藏与操作系统对象相关的数
据。这有助于简化程序数据和操作系统对象之间的关系。即使你从来不使用
这些内存分格,弄懂它们也将有助于你理解各种用户界面对象的操作。例如,
对话框控件(包括编辑框、列表框、组合框和按钮),使用窗口额外字节来
存储特定于控件的数据。
这种布局把硬件放在底部,顶部是通向应用程序的连接。尽管许多内存
类型可用,但最终都依赖于在进程地址空间内分配的私有页。除提供单进程
页外,Win32 还允许创建单线程数据。最后,还有一组用于堆分配的 API。
Visual C++ 提供自己的默认堆分配器,它建立在未用页的顶部。另外,
Win32 堆分配器还提供基本堆支持,并且,为了向后兼容还支持两种类型的
Win16 堆分配函数。
微软操作系统似乎总是成对地提供内存分配 API。例如,16 位 OS/2
API 提供 DosAllocSeg() 和 DosSubAlloc() 分别用于分配段和子段。
(真的,微软确实建立了这部分 OS/2 API。)Win16 提供了一对可比较的
分配例程,用 GlobalAlloc() 分配段,用 LocalAlloc() 把段划分成更
小的子段的块。
Win32 保持了这个传统,提供 VirtualAlloc() 来分配页。将 Heap-
Alloc() 用于子页的分配。它们是我们要讨论的前两种内存类型。两个系列
的分配器有时被称为“重量级”和“轻量级”分配器,因为该称呼抓住了两
系列分配器之间的关键区别:分配内存的间隔尺寸。
分配页
在 Windows 98(和 Windows NT)的分页式内存环境中,Win32 进程
可用的所有内存都驻留在分页的内存中,也就是说,驻留在这一页或另一页
中。它还支持一些子页分配器。只有两个 Win32 函数为进程的地址空间增
加新页。一个函数,VirtualAlloc(),分配私有页。另一个函数,MapVi-
ewOfFile(),分配共享页。其他所有的分配器都直接或问接地依赖于这两个
分配器来提供内存页。
Win32 私有页分配器是 VirtualAlloc()。除了分配私有进程页,它
还有另外一个作用。它可以保留内存地址范围但并不真正引起物理空间的消
耗,无论是基于 RAM 的页还是基于磁盘的页文件的页。这使得程序可以保
留非常大的地址范围,而不会引起页耗尽的问题。当需要时,再把一组内存
页映射到保留的地址范围。例如,对于几 GB 大小的数据库中的大块数据,
在需要时,每次把其中很小一块载人到“实在”内存中。要在保留地址的范
围内分配一个新页,程序调用同一个 VirtualAlloc() 函数来提交内存。
要分配共享页,Win32 进程需要调用 MapbViewOfFile()。这个函数
的名称很陌生,但是它反映了这个函数的双重角色。一方面,MapViewOf-
File() 创建内存映射文件的视图。另一方面,它分配共享内存,这是描述
内存映射文件的另一种方式,内存映射文件有时就是系统分页文件。在本章
的后面部分我们将讨论共享页的分配。
现在,我们重点关注特定于进程的页的分配。首先,我们看一看 Vir-
tualAlloc() 如何让你预留地址空间。然后讨论页的分配。最后,我们将
谈到 Win32 程序中的堆栈分配。
预留地址空间
Win32 允许程序员管理进程地址空间。大多数程序员并不把地址空间看
作需要管理的资源。毕竟,我们有数十亿的地址,为什么还要斤斤计较呢?
但是在有些情况下,直接控制地址空间对我们会很有帮助。
例如,当需要为可能会不断增长的数据对象动态分配内存块时,管理地
址空间就会大有益处。尽管这并不总是必需的,但把对象的所有部分保存在
连续的内存中会简化数据处理。然而,为了允许一个块在连续的内存中增长,
大多数操作系统都要求你预先分配超出实际需要的内存。
分配的内存超出了实际需要是一种浪费,但有时这是满足你的需要的最
简单的办法。否则,当一个对象增长得超过所在内存块时,就必须转移到更
大的空间中。从处理器时间来看,移动巨大的对象代价很高,而且会产生地
址空间碎片。当然,除预分配更多空间外,还有另外一个选择。在对象增长
时,你可以移动对象,也可以用链表把不同数据块在逻辑上链接起来。但是
这会产生其他问题,如链表管理的开销和由于数据处理的复杂性而产生的程
序中的错误。
Win32 API 为这个问题提供了一个更好的解决方案。避免由于分配比需
要得多的内存而产生浪费,改为按给定数据块分配刚好够用的页数。(平均
起来,浪费的内存是半个页,即 2048 字节。)但是这是解决方案的后半部
分。
在为可能增长的对象分配任何页之前,你需要预留足以容纳可预料的最
大尺寸的地址范围。预留地址范围以后,就可以通过从预留地址范围内提交
页而分配内存。开始,你只需按最初的需要分配。当需要的内存增加时,再
提交更多的页。这样,避免了过多分配的浪费,充分利用了内存。另外,加
入到现有页集合中的新页可以按和已经分配的页连续的地址范围访问。
这个例子有助于你的理解。假设你需要把一个需要占用 100K 的文件读
入内存,而且预计这个对象不会增长得超过 500K。使用 Win32 虚拟内存
API,首先预留一个 500K 的内存地址范围。在这个地址范围中,提交 100K
到实际的内存中,以便你可以把文件读入到内存中。当需要的内存量增长时,
你可以扩展已提交页的大小,以适应对象的增长。
弄明白什么时候真正消耗了内存很重要。特别是,预留的地址没有消耗
物理内存页(除用于虚拟内存管理器的地址清单的少量地址以外)。作为验
证,可以试着访问位于预留(但未提交)地址范围内的内存。你将得到一个
错误(一般保护错),如果没有处理它,将导致程序终止。
尽管预留地址空间并不能让你访问物理内存,但却是规划对象的增长必
需的第一步。否则,应用程序(或应用程序使用的 DLL)的另一部分可能会
使用超出分配区域的地址。如果发生这种情况,就又回到了前面的问题,不
得不在需要对象增长时移动整个数据对象。
要预留地址空间区域时,需调用 VirtualAlloc() 并传入 MEM_RESE-
RVE 标志。这是为 VirtualAlloc() 的第三个参数定义的三个标志之一。
函数自身的定义如下:
LPVOID VirtualAlloc(LPVOID lpvAddress,DWORD dwSize,
DWORD dwAction,DWORD dwAccess);
lpvAddress 参数是一个内存地址。当预留地址空间区域时, 设置为
NULL,使系统选取一个起始地址,如果你想要某个起始地址,也可以传入一
个非 NULL 值。当你把前面预留的地址提交到内存页时,这个值将向下舍入
到页(4K)边界。当预留地址空间区域时,这个值向下舍入到区段(64K)的
边界。
dwSize 参数是要预留或提交的字节数。提交时这个数值被向上舍入到
最近的页(4K)边界,预留时这个数值被向上舍人到最近的区段(64K)边界。
dwAction 参数的值,预留地址空间区域时为 MEM_RESERVE,把前面预
留的地址空间提交到内存页时为 MEM_COMMIT,当预留和提交内存时是二者
的组合 MEMM_RESERVE|MEM_COMMIT。(MEM_COMMIT 本身也可以完成同样
的工作。)
dwAccess 参数是指定页访问状态的标志域。在已定义的标志中,PAGE_
READWRITE 用于完全访问,PAGE_NOACCESS 用于禁止访问。(Windows 98
不支持 PAGE_GUARD。)
VirtualAlloc() 的返回值是内存区段的基地址,如果失败则返回值为
NULL 一组被预留的地址称作区段。无论是 Windows 98 还是 Windows NT
中,区段的间隔尺寸是 64K。换句话说,如果你预留一个字节的地址范围,
那么这个区段至少包含 64K 地址。如果每个区段都是 64K,那么整个 4GB
虚拟地址空间可以容纳 64K 个区段。
-- 独人独剑独马
浪迹天涯 ...
※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.99.74.233]
|
|