精华区 [关闭][返回]

当前位置:网易精华区>>讨论区精华>>编程开发>>C/C++>>技术精解:内存、进程、线程等>>Win98 内存管理>>Win98 内存管理(七)

主题:Win98 内存管理(七)
发信人: skyice()
整理人: skyice(2000-06-22 00:30:01), 站内信件
Win32 私有堆
  Win32 API 提供了一组函数,可以在多个私有堆中创建和分配内存。
Win32 页分配器应被用于分配大型对象,在 Windows 98 下按 4K 页的倍
数,而私有堆分配器为以子页计的数据项提供内存分配。
  因为 Win32 不提供任何用于进程间堆共享的机制(尽管同一进程中的
两个线程可以很容易地共享一个堆),所以我们把 Win32 堆支持作为私有
堆分配器。因为这些堆是进程私有的,它们创建在只对单个进程可见的内存
页中。糟糕的是,在 Win32 进程间共享内存时,Win32 并未为你组织这些
内存。你必须自己完成这样的工作。
  Win32提供了创建多个堆的能力,所以你可以用适合你的方式分隔数据。
例如,大型应用程序可能会创建多个堆,把属于不同子系统的数据放在不同
的堆中。这样在一个子系统中产生的内存泄漏或非法指针问题对其他子系统
的操作造成损害的可能性较小。
  创建多个堆的另一个原因是隔离不同大小的对象。对于通用分配器,当
大量各种各样不同大小的对象申请和释放时,就会产生内存碎片。例如,创
建某个类私有的堆时,就会切下堆碎片从而导致内存浪费。

堆实现的背景
  下表总结了在 Windows 98 和 Windows NT 这两种 Win32 实现中的
堆分配例程的操作特征。你也许会感到惊讶,这两个操作系统竟以完全不同
的方式提供同一个 API。但考虑到它们是由微软公司中不同的开发小组、根
据不同的目标和限制条件开发的,你就不会感到太奇怪了。

               堆分配例程的操作特征
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  标志                 值           说明
─────────────────────────────────
  内存分配问题         Windows 98   Windows NT
─────────────────────────────────
  最小对象尺寸         12字节       8字节
─────────────────────────────────
  每个对象的附加开销   4字节        8字节
─────────────────────────────────
  分配间隔尺寸         4字节        8字节
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  我们用一两句话来解释一下这个表。最小对象尺寸无需说明。当分配小
对象时,这个值标识了被分配的内存的最小数量。无论是 Windows 98,还
是 Windows NT,如果把每个对象的附加开销都加在一起,每次分配最少占
16 字节。
  单个对象的附加开销的值就是每个堆对象的头标的大小。在这两个操作
系统中,头标就存储在被分配数据区的起始地址下面。尽管在从堆分配空间
时,一般不考虑这个附加开销,但它确实是实际存在的花费,当你评估程序
内存的使用时,应当把它计算在内。
  在 Windows 98 上,堆分配器的间隔尺寸是 4 字节。例如,当向堆分
配器申请 15 字节时,它就提供 16 字节的区域。加上 4 字节头标,你的
15 字节的申请实际用去了 20 字节的内存空间。
  正如下表中所示,Windows 98 的分配附加开销和间隔尺寸都比 Win-
dows NT 小。原因就是 Windows 98 的目标机器是配置较低的带 4MB RAM
的 80486 系统。而 Windows NT 则是为具有更多 RAM 和更强大的处理器
而建立的。Windows 98 中吝啬的堆分配器有利于操作系统运行于资源贫乏
的机器上。
  在比较内存分配器时,需要一种衡量其区别的方法。一种方法就是计算
分配器对你分配的对象大小收取的“内存税”。在我们的例子中,申请 15 
字节,花费了 20 字节的区域,税率是 5/15 或者 33%。尽管 5 个字节好
象不很昂贵,但 33% 的税率却使相对费用相当可观。(用人来比喻,33% 
的销售税简直是极端无理,33% 的收入调节税或许还说得过去。)在 Win-
dows NT 上,同样的 15 字节的对象将用掉 24 字节,比你计划存储的实
际字节数多了 60%。
  如果你的 Win32/MFC 程序中的一部分使用了私有堆函数,就必须在两
个平台上做广泛测试。否则,就有可能漏过在一个平台上出现而在另一个平
台上不会出现的微小错误。例如,在分配的对象末尾写超了 6 个字节的错
误在 Windows NT 下可能不会被注意到。但在 Windows 98 上,这样的错
误将重写其他对象。
  在 Windows 98上,堆总是驻留在单个进程的、私有的内存范围中,即
4MB 到 2 GB 之间。尽管在 Windows NT 进程中堆的位置不会告诉你它是
私有的还是共享的,但实际上,在 Windows NT上堆总是私有的。若想验证
这一事实,可以对一个堆的地址调用 VirtualQuery()。它将报告堆内存页
是私有(MEM_PRIVATE)页。
  在 Windows 98 和 Windows NT 上,尽管有一些操作特征不同,但从
私有页分配私有堆的方式是类似的。创建堆的Win32 API是 HeapCreate(),
该函数调用 VirtualAlloc() 两次。第一次调用 VirtualAlloc() 为预期
的堆的增长预留内存地址空间。第二次调用在这个地址范围的开头提交内存
页。

Win32 堆 API
  下表总结了 Win32 的堆管理函数。大多数相当直观。关于调用这些函
数的细节内容,请参看 Win32 文档或联机帮助数据库。一些基本的设计和
实现细节还是值得讨论的。其中最主要的就是每个 Win32 进程都得到一个
堆的事实。大多数 Win32 堆函数带有一个堆句柄作为参数。要得到系统
提供的堆的句柄,可以调用 GetPProcessHeap()。使用返回的句柄,就可
以在堆中为自己分配内存。

            Win32 私有堆支持函数
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  函数                说明
──────────────────────────────────
  GetProcessHeap()    检索默认进程堆的句柄
──────────────────────────────────
  GetProcessHeaps()   在当前进程中检索所有可用堆的堆句柄列表
──────────────────────────────────
                     从堆句柄指示的堆中分配内存。Win16 对象可被
  HeapAlloc()         分配为可移动或可丢弃的,而这里与 Win16对象
                      不同,所有的对象都是固定的。本章后面对此有
                      进一步讲述。
──────────────────────────────────
  HeapCompact()       通过合并自由区域并在可能时释放不必要的页来
                      执行一些堆的清理工作。
──────────────────────────────────
                      在进程地址空间中创建一个新堆。调用Virtua-
  HeapCreate()        lAlloc() 两次:第一次预留地址范围,然后再
                      调用一次,为堆提交所需的最小内存。
──────────────────────────────────
  HeapDestory()       销毁由 HeapCreate() 创建的堆。
──────────────────────────────────
  HeapFree()          释放调用 HeapAlloc() 或 HeapReAlloc() 分
                      配的对象。
──────────────────────────────────
  HeapLock()          获得堆的临界区。只有在两个或多个线程都在访
                      问一个堆时才会用到。调用这个函数后,应当尽
                      快调用 HeapUnlock()。
──────────────────────────────────
  HeapReAlloc()       改变调用 HeapAlloc() 或上次调用 HeapReA-
                      lloc() 分配的对象的大小。
──────────────────────────────────
                      返回一个对象的大小。在 Windows 98上,这是
  HeapSize()          已分配区域(舍入到下一个 4 字节边界)的大
                      小,而不是申请内存的大小。在Windows NT上,
                      HeapSize() 返回调用 HeapAlloc() 或 Heap-
                      ReAlloc() 时申请内存的大小。
──────────────────────────────────
  HeapUnlock()        释放对堆的临界区的所有权。参见 HeapLock()。
──────────────────────────────────
  HeapValidate()      检查整个堆或堆中的一个对象的完整性。
──────────────────────────────────
  HeapWalk()          列举堆中分配的所有对象。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  关于 Win32 堆的另一个重要问题是串行性(serialization)。几乎
每一个堆函数都有一个堆分配标志参数,其中一个可能的标志是 HEAP_NO_
SERIALIZE。这个标志禁止堆的默认操作,即由多个线程串行(或者同步)
访问堆的操作。请注意,这里使用的术语“串行”(serialize)和 C++ 
中的“serialization”毫无关系。换句话说,这个上下文中的串行性与在
内存和磁盘问转移数据无关。当启用这个特性时,如果在同一时刻有两个线
程都想修改一个堆(即添加或删除数据对象),堆的串行性支持可以强制它
们排队等待。
  线程是允许在 Win32 进程中创建多个独立调度实体的 Win32 特性。
Win32 堆函数允许多个线程在同一时刻访问堆。但是默认状态下,Win32 
堆函数避免在同一时刻有两个线程修改堆。用来完成这个任务的操作系统
对象就是临界区。
  在应用程序中只有一个线程时使用 HEAP_NO_SERIALIZE 标志。这可以
帮助堆分配函数运行得稍快一点,因为串行性的确耗费了一点处理器时间。
我们并不是暗示它实现得不好,但是在分配大量对象时,这样的一小点节省
就可以积累起来。
  HeapValidate() 函数检查特定堆的完整性。当你怀疑有一个非法指针
或缓冲区溢出引起了问题时,这个函数合非常有用。即使你有所防备地编写
代码,就象在 Steve Maguire 的《Writing Solid Code》中详细描述的
那样,你还是有可能发现内存什么时候被重写了。有策略地放置一些对这个
函数的调用对准确找到问题的原因大有帮助。
  另一个可能很有用的函数是 HeapWalk()。Win16 程序员可能会想起微
软的HeapWalker 实用程序。命名为 Luke Heap Walker(依照“星球大战”
中的角色 Luke Sky Walker 的名称取名)的这个 Win16 Heap Walker 实
用程序可以显示各种堆的内容。这个函数让你可以编程检查在一个堆中分配
的所有数据对象。为帮助你建立自己的内存检测和诊断例程,这个函数将让
你知道关于 Win32 堆的几乎任何内容。

--
欢迎您到C语言版来!
欢迎光临BBS系统版!

※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.99.91.236]

[关闭][返回]