WIN32汇编语言教程:第10章 内存管理和文件操作 · 10.1 内存管理(4)

对应的资源文件Fragment.rc如下:

//##################################################################
#include              <resource.h>
//##################################################################
#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 内存管理(3) 下页:第10章 内存管理和文件操作 · 10.1 内存管理(5)

第10章 内存管理和文件操作

版权所有 © 中山市飞娥软件工作室 证书:粤ICP备09170368号