WIN32汇编语言教程:第10章 内存管理和文件操作 · 10.1 内存管理(5)
10.1.4 堆管理函数
Windows的“堆”分为默认堆和私有堆两种。默认堆是在程序初始化时由操作系统自动创建的,所有的标准内存管理函数都是在默认堆中申请内存的;而私有堆相当于在默认堆中保留了一大块内存,用堆管理函数可以在这个保留的内存块中分配内存。
一个进程的默认堆只有一个,而私有堆可以被创建多个。使用私有堆的缺点是分配和释放内存块的过程中多了一个扫描堆中的内存链的过程,所以单从分配内存的角度来讲,在私有堆中分配内存速度似乎要慢一点。
但实际上,有些时候使用私有堆可能更有好处。
首先,可以使用默认堆的函数有多种,而它们可能在不同的线程中同时对默认堆进行操作,为了保持同步,对默认堆的访问是顺序进行的,也就是说,在同一时间内每次只有一个线程能够分配和释放默认堆中的内存块。如果两个线程试图同时分配默认堆中的内存块,那么只有一个线程能够进行,另一个线程必须等待第一个线程的内存块分配结束之后才能继续执行。而私有堆的空间是预留的,不同线程在不同的私有堆中同时分配内存并不会引起冲突,所以整体的运行速度可能更快。
其次,当系统必须在物理内存和页文件之间进行页面交换的时候,系统的性能会受到很大的影响,在某些情况下,使用私有堆可以防止系统频繁地在物理内存和交换文件之间进行数据交换,因为将经常访问的内存局限于一个小范围地址的话,页面交换就不太可能发生,把频繁访问的大量小块内存放在同一个私有堆中就可以保证它们在内存中的位置接近。
再则,使用私有堆也有利于封装和保护模块化的程序。当程序包含多个模块的时候,如果使用标准内存管理函数在默认堆中分配内存,那么所有模块分配的内存块是交叉排列在一起的,如果模块A中的一个错误导致内存操作越界,可能会覆盖掉模块B使用的内存块,到模块B执行的时候出错了,我们却很难发现错误的源头来自于模块A。如果让不同的模块使用自己的私有堆,那么它们使用的内存就会完全隔离开来,虽然越界错误仍然可能发生,但很容易跟踪和定位。
最后,使用私有堆也使大量内存的清理变得方便,在默认堆中分配的内存需要一块块单独释放,但将一个私有堆释放后,在这个堆里的内存就全部被释放掉了,并不需要预先释放堆中的每个内存块,这样非常便于模块的扫尾工作。
1. 私有堆的创建和释放
创建私有堆的函数是HeapCreate:
invoke HeapCreate,flOptions,dwInitialSize,dwMaximumSize .if eax && (eax < 0c0000000h) mov hHeap,eax .endif
flOptions参数是标志,用来指定堆的属性,可以指定的属性有HEAP_NO_SERIALIZE和HEAP_GENERATE_EXCEPTIONS两种。
HEAP_GENERATE_EXCEPTIONS标志用来指定函数失败时的返回值,不指定这个标志的话,函数失败时返回NULL,否则返回一个具体的出错代码,以便于程序详细了解出错原因。出错代码的定义值都大于0c0000000h,因为0c0000000h开始的地址空间为系统使用,分配的内存地址不可能高于这个地址,所以检测函数执行是否成功的时候可以使用上面的测试语句来比较返回值是否在0~0c0000000h之间。
HEAP_NO_SERIALIZE标志用来控制对私有堆的访问是否要进行独占性的检测,前面曾经提到在默认堆中申请内存块的操作是顺序进行的,多个线程同时申请内存的请求只有一个能马上执行,其他将处于等待状态,对于一个私有堆来说,这个限制仍然存在,当从堆中分配内存时,系统有下面的操作步骤:
(1)遍历已分配的和空闲的内存块的链接表。
(2)寻找一个空闲内存块的地址。
(3)通过将空闲内存块标记为“已分配”来分配新内存块。
(4)将新内存块添加给内存块链接表。
当两个线程几乎同时在同一个堆中申请内存时,如果第一个线程执行了(1)、(2)两步的时候被系统切换到第二个线性,线程2同样又执行(1)、(2)两步,那么它们找到的空闲内存块就会是同一块内存,结果可想而知。解决问题的办法就是让单个线程独占对堆和它的链接表的访问权,当一个线程全部执行了这4个步骤后才允许第二个线程开始第一个步骤。
在用默认参数建立的堆中申请内存,系统会进行独占的检测工作,当然这要花费一定的系统开销。但是当以下情况存在时,可以保证不会同时有多个线程在同一个堆中申请内存:
● 进程只使用一个线程。
● 进程使用多个线程,但是每个线程只访问属于自己的私有堆。
● 进程使用多个线程,但程序中已经有其他措施来保证它们不会同时去访问同一个私有堆。
在这些情况下,可以指定HEAP_NO_SERIALIZE 标志来建立私有堆,这样建立的堆不会进行独占性的检测,访问速度可以更快。
参数dwInitialSize指定创建堆时分配给堆的物理内存,随着堆中内存的分配,当这些内存被使用完时,堆的长度可以自动扩展。dwMaximumSize参数指定了能够扩展到的最大值,当扩展到最大值时再尝试在堆中分配内存的话就会失败,这个值决定了系统给堆保留的连续地址空间的大小,函数会自动将这两个参数的数值调整为页面大小的整数倍。如果dwMaximumSize参数的值指定为0,那么堆没有最大值限制,扩展范围只受限于空闲的内存总量。如果dwMaximumSize指定为非0值,在堆中申请的最大单个内存块不能大于7FFF8h(相当于524 KB),dwMaximumSize指定0的话就没有这个限制。
如果一个私有堆不再需要了,可以通过调用HeapDestroy函数将它释放:
invoke HeapDestroy,hHeap
释放私有堆可以释放堆中包含的所有内存块,也可以将堆占用的物理内存和保留的地址空间全部返还给系统。如果函数运行成功,返回值是TRUE。当在进程终止的时候没有调用HeapDestroy函数将私有堆释放时,系统会自动释放。
虽然在默认堆中的内存申请主要使用标准内存管理函数,而堆管理函数的主要管理对象是私有堆,但是如果编程者愿意的话,也可以用堆管理函数在默认堆中分配内存,毕竟默认堆也是一个堆,但这样的话首先需要有一个句柄来代表默认堆,默认堆的句柄不能用HeapCreate来创建,但可以用GetProcessHeap函数来获取,这个函数没有输入参数,如果执行成功则返回默认堆的句柄。注意:这个句柄是“获取”的而不是“创建”的,所以不能调用HeapDestroy来释放它,如果对它调用HeapDestroy函数,系统会将它忽略。
2. 在堆中分配和释放内存块
如果要在堆中分配内存块,可以使用HeapAlloc函数:
invoke HeapAlloc,hHeap,dwFlags,dwBytes .if eax && (eax < 0c0000000h) mov lpMemory,eax .endif
hHeap参数就是前面创建堆时返回的堆句柄(或者使用GetProcessHeap函数得到的默认堆句柄),用来表示在哪个堆中分配内存,dwBytes是需要分配的内存块的字节数,dwFlags是标志,它可以是下面值的组合:
● HEAP_NO_SERIALIZE——当使用HeapCreate时指定了HEAP_NO_SERIALIZE标志,以后这个堆中使用的所有HeapAlloc函数都不进行独占检测。如果使用HeapCreate时没有指定HEAP_NO_SERIALIZE标志,可以在这里使用HEAP_NO_SERIALIZE标志单独指定对本次分配操作不进行独占检测。
● HEAP_GENERATE _EXCEPTIONS——如果申请内存失败函数返回具体的出错原因,而不仅返回一个NULL。同样,当使用HeapCreate时指定了此标志的情况下,在这里就不必再一次指定。
● HEAP_ZERO_MEMORY——将分配的内存用0初始化。
当函数分配内存成功的时候,返回值是指向内存块第一个字节的指针,如果分配内存失败,返回值要视dwFlags的设置,如果没有指定HEAP_GENERATE_EXCEPTIONS标志,那么返回值为NULL,否则,返回值可能是下面的数值:
● STATUS_NO_MEMORY——取值为0C0000017h,表示内存不够。
● STATUS_ACCESS_VIOLATION——取值为0C0000005h,表示参数不正确或者堆被破坏。
在堆中分配的内存块只能是固定地址的内存块,不像GlobalAlloc函数一样可以分配可移动的内存块。如果要释放分配到的内存块,可以使用HeapFree函数:
invoke HeapFree,hHeap,dwFlags,lpMemory
hHeap参数是堆句柄,lpMemory是HeapAlloc函数返回的内存块指针,dwFlags参数中也可以使用HEAP_NO_SERIALIZE标志,含义与使用HeapAlloc时相同。当函数执行成功的时候,返回值为非0值,执行失败则函数返回0。
对于用HeapAlloc分配的内存块,也可以使用HeapReAlloc重新调整大小:
invoke HeapReAlloc,hHeap,dwFlags,lpMemory,dwBytes .if eax && (eax < 0c0000000h) mov lpMemory,eax .endif
上页:第10章 内存管理和文件操作 · 10.1 内存管理(4) 下页:第10章 内存管理和文件操作 · 10.1 内存管理(6)