windows环境下32位汇编语言程序设计-第58章
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
10。1 内 存 管 理(4)
对应的资源文件Fragment。rc如下:
//》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
#include
//》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
#define ICO_MAIN 1000
#define DLG_MAIN 100
#define IDC_MEMORY 101
#define IDC_COUNT 102
#define IDC_INFO 103
//》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
ICO_MAIN ICON 〃Main。ico〃
//》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
DLG_MAIN DIALOG 308; 207; 130; 50
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION 〃碎片内存演示〃
FONT 9; 〃宋体〃
{
RTEXT 〃申请内存总数:〃; …1; 7; 8; 60; 8
EDITTEXT IDC_MEMORY; 69; 5; 55; 12;
ES_AUTOHSCROLL | ES_READONLY | WS_BORDER | WS_TABSTOP
RTEXT 〃申请次数:〃; …1; 7; 21; 60; 8
EDITTEXT IDC_COUNT; 69; 19; 55; 12;
ES_AUTOHSCROLL | ES_READONLY | WS_BORDER | WS_TABSTOP
LTEXT 〃〃; IDC_INFO; 7; 37; 120; 8
}
//》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
程序在WM_INITDIALOG消息中建立了一个线程来循环申请内存(相当于在后台执行_ProcThread子程序,与多线程相关的内容请参见第12章)。全局变量dwCount记录了申请的次数,每次申请内存就将它的值加1。dwTotalMemory记录了程序申请到的内存总数,每申请一个1 MB的内存,程序将它的值加上1 000 000,每次用GlobalReAlloc缩小内存块,则将它的值减去999 900。当最后申请内存失败的时候,repeat循环结束。
在Windows 2000下运行一下程序以验证结果,几秒的运行中,显示的计数不断增加,最后的结果如图10。3所示。
图10。3 内存碎片化的演示结果
结果和预想的一样,经过2 027次的操作,只保留了近202 700 B的内存,程序就成功地“谋杀”了所有的地址空间,让整个2 GB中间充满了碎片,以至于连1 MB大小的内存也无法申请了!当程序在Windows 9x中运行时,由于9x系统在高端和低端轮换分配内存块,所以同样的办法就不会产生内存碎片,但是如果在循环中先Alloc两次、然后Realloc两次的话仍然可以造成内存碎片化。
虽然这是一个极端的情况,但在现实中会发生吗?会的!例如编写一个遍历二叉树的程序,每增加一个结点的时候申请一块内存,用来存放指向其他结点的指针以及附加在结点上的数据,当结点处理完毕后缩小内存块,只留下指针数据,那么情况就和演示程序类似,当树的结点足够多的时候,经过一段时间的操作,内存中就会充满碎片。
解决内存碎片化的办法很简单,因为碎片之间有大量的内存是空闲的,只要允许Windows移动小块的在用内存,就可以将碎片合并成大块的空闲内存,但是在用内存被移动后,程序中对应的指针也要随着改变,不然就会访问到错误的地址,而且,在使用内存的过程中,内存需要有个锁定的过程,否则用到一半的时候被Windows移动了,结果依然是错误的,只有程序将内存解锁,Windows才可以自由移动它们,这就引伸出了可移动内存块的概念和操作的基本方法。
要申请一个可移动的内存块,使用的函数还是GlobalAlloc,但需要使用不同的参数:
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,dwBytes
。if eax
mov hMemory,eax
。endif
GMEM_MOVEABLE标志指定了分配的内存是可移动的,GMEM_ZEROINIT同样表示将申请到的内存块的内容初始化为0(也可以用GHND标志,它就相当于GMEM _MOVEABLE or GMEM_ZEROINIT);如果内存申请失败,eax中返回NULL,成功的话返回值是一个句柄而不是内存指针,用户需要保存这个句柄,在锁定或释放内存的时候还要用到它。一个进程可以申请的可移动内存的块数最大不能超过65 536个,申请固定内存块时则没有数量限制。
要使用可移动内存之前,需要把它锁定,这相当于告诉Windows现在程序要使用这块内存了,不能将它移动,锁定内存使用GlobalLock函数:
invoke GlobalLock;hMemory
。if eax
mov lpMemory,eax
。endif
函数的入口参数是GlobalAlloc返回的内存句柄,如果锁定成功,函数返回一个指针,程序可以用使用固定内存块同样的方法来使用它;如果锁定失败,则函数返回NULL。每次锁定返回的指针位置可能是不同的,但内存块中的数据不会变化。
当程序暂时不需要操作这块内存的时候,应该将它解锁,否则和使用固定的内存块就没有区别了,解锁使用GlobalUnlock函数:
invoke GlobalUnlock;hMemory
函数的参数同样是GlobalAlloc返回的句柄,解锁成功的话函数返回非0值。读者可能有个问题:在多线程的程序中,两个地方同时锁定内存,但当一个地方还在使用的情况下另一个地方却调用GlobalUnlock将内存解锁了怎么办?其实不用担心这个问题,Windows为每个可移动的内存句柄维护一个锁定计数,每次锁定内存的时候计数加1,解锁的时候计数减1,只有当计数为0的时候内存才真正被解锁,所以只要程序中的GlobalLock函数和GlobalUnlock函数是配对的,就不用担心这个问题。
要释放一个可移动的内存块,同样使用GlobalFree函数:
invoke GlobalFree,hMemory
但使用的参数是GlobalAlloc返回的内存句柄,如果释放成功,函数返回NULL。不管内存当前是否处在锁定状态,都可以被成功释放。
调整可移动内存块的大小,同样使用GlobalReAlloc函数:
invoke GlobalReAlloc,hMemory,dwBytes,GMEM_ZEROINIT or GMEM_MOVEABLE
如果调整成功,返回值就是输入的hMemory,失败的话返回值是NULL。即使内存块在锁定状态,函数仍然可以调用成功,但这时候内存块可能已经被移动了位置,原来用GlobalLock函数获取的指针可能已经失效了,所以调整可移动内存块的大小最好还是先将内存解锁,等调整完毕以后再锁定使用。
由于使用可移动的内存块多了一个锁定的动作,速度自然要比使用固定的内存块要慢一点,但固定内存块又存在碎片问题,程序中使用哪种方法有个取舍的问题。如果程序要频繁地分配和释放不定长的内存块,内存的碎片化现象就比较严重,特别是当程序长时间运行时,这种情况下使用可移动内存块比较好;如果程序只进行少量的内存操作,或者虽然频繁分配和释放内存,但使用的内存块长度都是一样的,则使用固定内存块可以节省时间。
3。 可丢弃的内存块
分配可移动内存块的时候还可以配合GMEM_MOVEABLE标志使用GMEM_DI SCARDABLE标志,这样生成的内存块是可丢弃的内存块,表示当Windows急需内存使用的时候,可以将它从物理内存中丢弃,可丢弃的内存块首先必须是可移动的内存块。函数调用如下:
invoke GlobalAlloc,GHND or GMEM_DISCARDABLE,dwBytes
。if eax
mov hMemory,eax
。endif
当用GlobalLock锁定内存的时候如果返回NULL指针,表示内存已经被Windows丢弃了,当然其中的数据也丢失了,程序需要重新生成数据。当内存块被丢弃的时候,内存句柄还是有效的,如果程序还要使用这个句柄,那么可以对它使用GlobalReAlloc函数来重新分配内存。
当可丢弃内存块的锁定计数为0时,程序也可以使用GlobalDiscard函数主动将它丢弃,这和Windows将它丢弃的效果是一样的:
invoke GlobalDiscard,hMemory
使用内存函数时有两个地方需要特别注意:
(1)NULL指针的检测——GlobalAlloc函数和GlobalLock函数都可以返回内存指针,在使用指针前一定要检测它的有效性,如果使用了函数执行失败而返回的NULL指针来访问数据,会导致程序越权访问不该访问的地方,从而被Windows毫不留情地终止掉,这就是例子代码中总是有个if语句来判断eax是否为NULL的原因。
(2)注意访问越界问题——越界操作也会引起越权访问,千万不要到超出内存块长度的地方去访问,例如,使用lstrcpy之类的函数处理字符串之前,先用lstrlen检测字符串长度是一个好习惯。
4。 获取内存块的信息
标准内存管理函数中的其他函数GlobalFlags,GlobalHandle和GlobalSize用来获取已分配内存块的一些信息。
GlobalFlags函数主要用来获取可移动内存块当前的锁定计数,也可以用来检测可丢弃内存块是否已经被丢弃。对一个hMemory调用GlobalFlags函数如下所示:
invoke GlobalFlags,hMemory
如果不是返回GMEM_INVALID_HANDLE,则表示调用成功,这时返回值的低8位是内存块的锁定计数,程序可以用GMEM_LOCKCOUNT对获取计数值进行and操作(在Windows。inc头文件中,GMEM_LOCKCOUNT定义为0ffh):
invoke GlobalFlags,hMemory
and eax,GMEM_LOCKCOUNT
mov dwLockCount,eax
返回值的其他数据位可能包含下列标志:
● GMEM_DISCARDABLE 表示内存块是可丢弃内存块。
● GMEM_DISCARDED 表示内存块已经被丢弃。
GlobalHandle可以从GlobalLock函数得到的lpMemory值获取其对应的hMemory,而GlobalSize函数可以获知一个内存块的尺寸。
来源:电子工业出版社 作者:罗云彬 上一页 回书目 下一页
上一页 回书目 下一页
第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 标志来建立私