精华区 [关闭][返回]

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

主题:Win98 内存管理(六)
发信人: skyice()
整理人: skyice(2000-06-22 00:29:47), 站内信件
编译器内存分配
  尽管理解页的分配扩展了你关于 Win32 API 的知识,但大多数MFC 程
序员可能依赖于默认的 Visual C++ 分配器来获得动态分配的内存。由于这
个原因,我们在讲述原始 Win32 堆分配器之前先讲编译器内存分配器。
  编译器分配器的一个重要特征就是基于单模块创建各自的堆。(根据微
软的用法,术语模块(module)不是指程序源文件,而是指可执行映象,诸
如.EXE 程序或 .DLL 动态链接库。)因为应用程序可以使用其动态链接库
为它创建任何内存对象(因为全部存在于一个地址空间中),所以在建立动
态链接库时,这是一个很重要的因素。然而,如果 DLL 从内存中退出,那
么 C 运行库将破坏编译器为那个 DLL 创建的堆。
  当确定使用哪个分配器时,一个重要问题是每个分配的对象消耗内存的
多少。我们在讨论 VirtualAlloc() 时没有提出这个问题,那是因为页分
配的开销相对于页的大小而言是很小的。但是因为堆分配器常常用来分配相
当小的内存块,每次分配的开销和间隔尺寸的问题就变得重要了。
  MFC 没有使用原始 Win32 分配器,而是使用一个自定义分配器(原始
分配器是指 HeapAlloc() 以及相关的 API 函数)。其原因主要是历史造
成的。Windows NT 3.1 上的 Win32 堆分配器效率很低。若使用 32 字节
的间隔尺寸和每个对象 16 字节的开销,事实证明当分配大量的小对象时,
这种费用太高。正如我们将要讨论的,对堆的支持在 Windows NT 3.5 中
有了显著改善,Windows 98 上甚至更好。在有些地方,微软可能修改MFC,
使它依赖于操作系统分配器,而不再依赖它们自己的分配器。为让你明白什
么促成了这一改变,我们将讨论默认的 Visual C++ 堆分配器。

MFC 的默认内存分配器
  MFC 默认的分配器是一个换了副新面孔的老朋友。这个老朋友(C程序
员会很熟悉)是 malloc()。它的新面孔是一个特定于 Win32 的实现,依
赖 VisualAlloc()为它的堆分配页。
  分配每个堆对象的代价是 12 字节。这 12 个字节按如下方式使用:
每个堆块有一个 4 字节的头标以及一个块描述符链表上的 8 字节结点。
结果是分配一个 24 字节的缓冲区要花费 36 字节。这有一个例子:
  // Data bytes:           24
  // + Header bytes:       4
  // + Block descriptor:   8
  //           ---------------
  // Total Cost: 36 bytes
  Void * pData = malloc(24);

  另一个分配问题是关于分配间隔尺寸的。分配间隔尺寸是需要特定大小
时的取整因子或倍数。默认的分配器间隔尺寸为 4 字节。对于大量对象。
由于舍入产生的浪费大约是 2 字节,这是个相对很小的浪费数量。
  Visual C++ 的默认分配器是一个简单的通用分配器。在堆中搜索满足
分配要求的块时,使用“最先满足”算法。搜索过程中,它将合并毗连的的
自由块。必要时,它按 64K 的增量向它的自由内存添加更多的页。每个区
段都可以增长到 1MB,分配器自己可以容纳 64 个区段,合计为一个 64MB
的堆。
  当你建立一个发行版本的 MFC 程序时,对操作符new()的调用(无论
对 CObject 派生对象还是非 CObject 派生对象)都会引起对 malloc() 
的调用。发行版本的程序中我们所关心的特征是没有定义 __DEBUG 预处理
符号。有半数 MFC 链接库是为发行建立方式而预留的,它们不包含调试版
本的诊断断言。  对于 MFC 程序的调试版本,内存分配就变得有意思了。
建立调试版本时__DEBUG 预处理符号被定义。__DEBUG 的定义触发了 MFC 
诊断分配器,使它可用。(它还触发调试链接库的使用。)诊断分配器的源
代码在 AFXMEM.CPP 中。诊断分配器并不是 Visual C++ 分配器的代用品,
而是完成一些额外工作的一个层次,以帮助捕获某些常见的内存问题。最终,
调试分配器自己也调用 malloc()。在下面一节,我们将讲述诊断分配器怎
样工作,并提出一些利用它的方式。

MFC 诊断分配器
  这有一个问题,你的经理可能已经问过你,或者没准快要问了,如何找
到有关写操作超越了内存缓冲区边界的错误?有的开发人员依赖于运气和艰
苦的工作来避开编写这样的代码。然而,由于代码的动态特性,这种方式很
快就会复杂得难以管理。有的开发人员使用内存包装函数,建立这样的函数
来自动检测这一类问题。当检测到问题时,则发出通知警告开发人员。MFC 
提供了这种类型的内存包装函数,使某些类型的检测错误几乎完全自动化。
  MFC 诊断分配器定义了自己的全局操作符 new() 和自己的 CObject::
operator new()。它们最终都调用了 malloc(),但在此之前,都加入了一
些特征,以帮助捕获通常的内存错误。
  MFC 的诊断分配器能帮助找到很多问题,其中有内存泄漏(已分配的对
象未被释放),缓冲区下溢和上溢(向缓冲区开头的前面和结尾的后面写人),
使用未初始化的缓冲区以及使用已经释放的缓冲区。尽管诊断分配器不是总
能诊断出导致这些问题的原因,但它可以使你从正确的方向开始查找错误原
因。举个例子,如果某个内存块发生问题,诊断分配器可以指示出分配该内
存的源文件和代码行。
  要做到这些。诊断分配器必须存储附加的信息,它使用操作符 new() 
为每次分配增加36个字节。每个这样分配的对象都有一个 28 字节的头标,
用来存储分配的内存块的关键值。另外 8 个附加字节作为未用区:4 个在
对象前面,四个在对象后面。这些字节可以帮助我出超越分配的内存块边界
的写操作。(顺便提一句,这不同于运行在 Windows 98 和 Windows NT 
上的 Win32 进程所看到的未用地址空间。)下面是诊断分配器的内存块头
标的结构,在AFXMEM.CPP中定义如下:(不过,我在 vc6 中并没有找到)
  struct CBlockHeader
  {
    struct CBlockHeader * pBlockHeaderNext;
    struct CBlockHeader * pBlockHeaderPrev;
    LPCSTR                lpszFileName;
    int                   nLine;
    size_t                nDataSize;
    enum CMemorySize::blockUsage use;
    LONG                  lRequest;
    BYTE                  gap[nNoMansLandSize];
    // followed by:
    // BYTE               data[nDataSize];
    // BYTE               anothergap[nNomansLandSize];
    BYTE * pbData()
    {
      return (BYTE *)(this+1);
    }
  };

  尽管为每个分配的对象都加上 36 字节是一个不小的数目,不过不要忘
了这只是一个开发试验品。当你已经找出并排除掉程序中的所有内存处理问
题后,就可以创建程序的发行版本。顾名思义,那才是你发行到用户手中的
软件。尽管一些附加诊断字节的出现将消耗更多的内存并降低程序的运行速
度,但这是值得的。不管怎么说,软件开发人员常常拥有最快、最高性能的
系统。你可能极少注意到调试带来的性能降低。
  除分配一个内存块头标,诊断分配器还用已知的非 0 值填充内存。这有
助于检测各种各样的指针问题,包括缓冲区上溢、下溢和已释放对象的访问。
甚至已分配的区域也用一个已知值 0xCD 填充。知道这些可以帮助你确定一
个函数是否把数据写入到了缓冲区中。如果写入了,你就会看到数据。如果
没有写入,调试器会显示一个 0xCD 值的数组,而不是你想写入缓冲区的数
据(说句题外话,32 位 OLE 库用值 0x0BADF00D 填充空内存)。
  在诊断堆中,在已分配对象的开头和结尾都建立了“警戒”,MFFFC 称
之为未用区,以捕获缓冲区上溢和下溢问题。在产品代码中,无论写操作超
越了开头还是结尾都会导致程序崩溃、数据丢失以及用户不断受挫。然而在
调试建立方式中,被重写的对象很容易被找到,从而帮助你找到问题的来源。
  尽管 malloc() 的间隔尺寸是 4 字节,但默认分配器的间隔尺寸只有
1 字节。未用区的位置紧跟在申请的数据区的后面。哪怕你只写超了 1 字
节,也会触发一个断言。这使得调试版本的分配器比发行版本使用的分配器
严格得多。“差 1”(off-by-one)重写错误在发行版本中可能不会被检测
出来,但在调试版本中却会引起警告。
  下面是调试分配器用来填充内存的三个值,它们在 AFXMEM.CPP 中定
义如下:
  #define bNoMansLandFill 0xFD // fill no-man's land with this
  #define bDeadLandFill 0xDD // fill free objects with this
  #define bCleanLandFill 0xCD // fill new objects with this
  在同一个源文件中,就在这些值前面有一个注释块,解释了为什么选择
这些特定的值。这些注释可以总结如下:
  ◆使用非 0 填充值使内存重写更明显,因为大多数常用内存填充操作
都产生以 0 填充的数据。
  ◆使用常量使内存填充可以判定,这有助于使错误再现。然而,取决于
数据如何使用,非 0 值的使用有时也会掩盖错误。
  ◆算术奇数值有助于发现假定已清除的低位的错误,对 Macintosh 上
的陷井也有所帮助。
  ◆大数值(至少是字节值)在程序中不常见,有利于找到错误地址。
  ◆非典型值(也就是,不经常使用的值)很有用,因为它们一般会触发
代码中的初期检测。
  ◆对于未用区和自由块,如果你向这些地方中的任何位置存储数据,内
存完整性检查程序都会检测到它。
  在 MFC 的调试建立方式中,有几个全局变量使你可以控制诊断分配器
的操作。其中一个是存储在全局变量中的标志域 afxMemDF。这个整型变量
在 MFC 源文件 APPDATA.CPP 中定义如下:
  int afxMemDf = allocMemDF;
  这个默认设置打开调试分配器。在 AFX.H 中为这个标志定义的其他值
显示在下表中。
                     调试分配器设置
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  标志              值    说明
─────────────────────────────────
  n/a               0     关闭调试分配器
─────────────────────────────────
  allocMemDF        0x01  打开调试分配器,为每个分配的对象加上—
                          个头标和一个未用区。
─────────────────────────────────
  delayFreeMemDF    0x02  延迟释放内存以测试内存不足的处理。这也
                          有助于确保不会引用已释放的指针。
─────────────────────────────────
                          在每次调用分配器(包括 new 和 de)时以
                          及空闲时间处理期间都检查堆的完整性。当
  checkAlwaysMemDF  0x04  这个标志被设置时,AfxCheckMemory()被调
                          用以验证堆。你也可以随时自己这个函数以
                          校验堆的合法性。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  你还可以设置一个计数器 __afxBreakAlloc,以在分配次数达到某个
数目时进入调试器。当允许启用调试分配器时,它会记住被调用的次数,并
按序列标记每个分配的对象。当检测到泄漏时,诊断分配器就报告丢失的块
的序列号。当你使用 IDE 试器时,这个特征使你的程序在分配未释放的块
时,马上会被捕获,大致帮助你弄清楚内存泄漏的原因。__afxBreakAlloc
计数器在禁止状态时在 AFXMEM.H 中定义如下:
  // for debugging memory leaks
  Static LONG afxBreakAlloc = -1;
  但是把这个值设置为在进入调试器之前允许执行多少次分配,不过是小
事一桩。偶然地,你可能会想在代码的一个条件编译块中定义这个值,如下
所示。因为这个变量没有为发行版本定义:
  #ifdef DEBUG
  afxBreakAlloc = 1000;
  #endif
  诊断分配器提供的帮助你检测软件的另一个特性是分配异常处理函数。
就在分配对象之前,分配器调用异常处理函数链以询问是否应创建一个“人
为的”分配失败。通过仿真低内存环境,可以检测错误处理例程是否做了该
做的事。细节内容请参见 AFXMEM.CPP 中的 AfxSetAllocHook() 函数。
  为捕获重写堆的指针错误,可以申请堆校验。有两种方式可以做到这一
点。你可以简单地调用校验堆的 AfxCheckMemory(),也可以把 afxMemDF
诊断标志设置为 checkAlwaysMemDF 值。无论是分配内存还是释放内存,
这都会引起调用 AfxCheckMemory()。它甚至会引起在空闲时间对堆的检查
(在 CWinThread::OnIdle() 内,它在 THRDCODE.CPP 中)。
  诊断分配器也检测内存泄漏,也就是分配了但没有释放的对象。在调试
建立方式中,当 MFC 程序终止时,会自动调用 AfxDumpMemoryLeakss() 
函数。任何仍在堆中的对象都将显示在调试器的 Output 窗口中。要得到这
样的输出,必须在 IDE 调试器运行时运行你的程序。也就是说,你不能只
让 IDE 运行;还要从 Debug 菜单中选择 Go 命令(或对应的键盘命令)
来启动你的 MFC 程序。内存泄漏的检测对于堵住这些漏洞,从而使应用程
序有效地使用内存提供了极大帮助。

--
独人独剑独马
浪迹天涯 ...

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

[关闭][返回]