18堆栈.docx
- 文档编号:29852872
- 上传时间:2023-07-27
- 格式:DOCX
- 页数:21
- 大小:70.85KB
18堆栈.docx
《18堆栈.docx》由会员分享,可在线阅读,更多相关《18堆栈.docx(21页珍藏版)》请在冰豆网上搜索。
18堆栈
第18章堆栈
对内存进行操作的第三个机制是使用堆栈。
堆栈可以用来分配许多较小的数据块。
例如,若要对链接表和链接树进行管理,最好的方法是使用堆栈,而不是第15章介绍的虚拟内存操作方法或第17章介绍的内存映射文件操作方法。
堆栈的优点是,可以不考虑分配粒度和页面边界之类的问题,集中精力处理手头的任务。
堆栈的缺点是,分配和释放内存块的速度比其他机制要慢,并且无法直接控制物理存储器的提交和回收。
从内部来讲,堆栈是保留的地址空间的一个区域。
开始时,保留区域中的大多数页面没有被提交物理存储器。
当从堆栈中进行越来越多的内存分配时,堆栈管理器将把更多的物理存储器提交给堆栈。
物理存储器总是从系统的页文件中分配的,当释放堆栈中的内存块时,堆栈管理器将收回这些物理存储器。
Microsoft并没有以文档的形式来规定堆栈释放和收回存储器时应该遵循的具体规则,Windows98与Windows2000的规则是不同的。
可以这样说,Windows98更加注重内存的使用,因此只要可能,它就收回堆栈。
Windows2000更加注重速度,因此它往往较长时间占用物理存储器,只有在一段时间后页面不再使用时,才将它返回给页文件。
Microsoft常常进行适应性测试并运行各种不同的条件,以确定在大部分时间内最适合的规则。
随着使用这些规则的应用程序和硬件的变更,这些规则也会有所变化。
如果了解这些规则对你的应用程序非常关键,那么请不要使用堆栈。
相反,可以使用虚拟内存函数(即VirtualAlloc和VirtualFree),这样,就能够控制这些规则。
18.1进程的默认堆栈
当进程初始化时,系统在进程的地址空间中创建一个堆栈。
该堆栈称为进程的默认堆栈。
按照默认设置,该堆栈的地址空间区域的大小是1MB。
但是,系统可以扩大进程的默认堆栈,使它大于其默认值。
当创建应用程序时,可以使用/HEAP链接开关,改变堆栈的1MB默认区域大小。
由于DLL没有与其相关的堆栈,所以当链接DLL时,不应该使用/HEAP链接开关。
/HEAP链接开关的句法如下:
/HEAP:
reserve[,commit]
许多Windows函数要求进程使用其默认堆栈。
例如,Windows2000的核心函数均使用Unicode字符和字符串执行它们的全部操作。
如果调用Windows函数的ANSI版本,那么该ANSI版本必须将ANSI字符串转换成Unicode字符串,然后调用同一个函数的Unicode版本。
为了进行字符串的转换,ANSI函数必须分配一个内存块,以便放置Unicode版本的字符串。
该内存块是从你的进程的默认堆栈中分配的。
Windows的其他许多函数需要使用一些临时内存块,这些内存块是从进程的默认堆栈中分配的。
另外,老的16位Windows函数LocalAlloc和GlobalAlloc也是从进程的默认堆栈中进行它们的内存分配的。
由于进程的默认堆栈可供许多Windows函数使用,你的应用程序有许多线程同时调用各种Windows函数,因此对默认堆栈的访问是顺序进行的。
换句话说,系统必须保证在规定的时间内,每次只有一个线程能够分配和释放默认堆栈中的内存块。
如果两个线程试图同时分配默认堆栈中的内存块,那么只有一个线程能够分配内存块,另一个线程必须等待第一个线程的内存块分配之后,才能分配它的内存块。
一旦第一个线程的内存块分配完,堆栈函数将允许第二个线程分配内存块。
这种顺序访问方法对速度有一定的影响。
如果你的应用程序只有一个线程,并且你想要以最快的速度访问堆栈,那么应该创建你自己的独立的堆栈,不要使用进程的默认堆栈。
不幸的是,你无法告诉Windows函数不要使用默认堆栈,因此,它们对堆栈的访问总是顺序进行的。
单个进程可以同时拥有若干个堆栈。
这些堆栈可以在进程的寿命期中创建和撤消。
但是,默认堆栈是在进程开始执行之前创建的,并且在进程终止运行时自动被撤消。
不能撤消进程的默认堆栈。
每个堆栈均用它自己的堆栈句柄来标识,用于分配和释放堆栈中的内存块的所有堆栈函数都需要这个堆栈句柄作为其参数。
可以通过调用GetProcessHeap函数获取你的进程默认堆栈的句柄:
HANDLEGetProcessHeap();
18.2为什么要创建辅助堆栈
除了进程的默认堆栈外,可以在进程的地址空间中创建一些辅助堆栈。
由于下列原因,你可能想要在自己的应用程序中创建一些辅助堆栈:
•保护组件。
•更加有效地进行内存管理。
•进行本地访问。
•减少线程同步的开销。
•迅速释放。
下面让我们来详细说明每个原因。
18.2.1保护组件
假如你的应用程序需要保护两个组件,一个是节点结构的链接表,一个是BRANCH结构的二进制树。
你有两个源代码文件,一个是LnkLst.cpp,它包含负责处理NODE链接表的各个函数,另一个文件是BinTree.cpp,它包含负责处理分支的二进制树的各个函数。
如果节点和分支一道存储在单个堆栈中,那么这个组合堆栈将类似图18-1所示的样子。
图18-1将节点和分支存放在一起的单个堆栈
现在假设链接表代码中有一个错误,它使节点1后面的8个字节不小心被改写了,从而导致分支3中的数据被破坏。
当BinTree.cpp文件中的代码后来试图遍历二进制树时,它将无法进行这项操作,因为它的内存已经被破坏。
当然,这使你认为二进制树代码中存在一个错误,而实际上错误是在链接表代码中。
由于不同类型的对象混合放在单个堆栈中,因此跟踪和确定错误将变得非常困难。
通过创建两个独立的堆栈,一个堆栈用于存放节点,另一个堆栈用于存放分支,就能够确定你的问题。
你的链接表代码中的一个小错误不会破坏你的二进制树的完整性。
反过来,二进制树中的小错误也不会影响链接表代码中的数据完整性。
但是,你的代码中的错误仍然可能导致对堆栈进行杂乱的内存写操作,不过出现这种情况的可能性很小。
18.2.2更有效的内存管理
通过在堆栈中分配同样大小的对象,就可以更加有效地管理堆栈。
例如,假设每个节点结构需要24字节,每个分支结构需要32字节。
所有这些对象均从单个堆栈中分配。
图18-2显示了单个堆栈中已经分配的若干个节点和分支对象占满了这个堆栈。
如果节点2和节点4被释放,堆栈中的内存将变成许多碎片。
这时,如果试图分配分支结构,那么尽管分支只需要32个字节,而实际上可以使用的有48个字节,但是分配仍将失败。
图18-2变成碎片的单个堆栈包含若干个节点和分支对象
如果每个堆栈只包含大小相同的对象,那么释放一个对象后,另一个对象就可以恰好放入被释放的对象空间中。
18.2.3进行本地访问
每当系统必须在RAM与系统的页文件之间进行RAM页面的交换时,系统的运行性能就会受到很大的影响。
如果经常访问局限于一个小范围地址的内存,那么系统就不太可能需要在RAM与磁盘之间进行页面的交换。
所以,在设计应用程序的时候,如果有些数据将被同时访问,那么最好把它们分配在互相靠近的位置上。
让我们回到链接表和二进制树的例子上来,遍历链接表与遍历二进制树之间并无什么关系。
如果将所有的节点放在一起(放在一个堆栈中),就可以使这些节点位于相邻的页面上。
实际上,若干个节点很可能恰好放入单个物理内存页面上。
遍历链接表将不需要CPU为了访问每个节点而引用若干不同的内存页面。
如果将节点和分支分配在单个页面上,那么节点就不一定会互相靠在一起。
在最坏的情况下,每个内存页面上可能只有一个节点,而其余的每个页面则由分支占用。
在这种情况下,遍历链接表将可能导致每个节点的页面出错,从而使进程运行得极慢。
18.2.4减少线程同步的开销
正如下面就要介绍的那样,按照默认设置,堆栈是顺序运行的,这样,如果多个线程试图同时访问堆栈,就不会使数据受到破坏。
但是,堆栈函数必须执行额外的代码,以保证堆栈对线程的安全性。
如果要进行大量的堆栈分配操作,那么执行这些额外的代码会增加很大的负担,从而降低你的应用程序的运行性能。
当你创建一个新堆栈时,可以告诉系统,只有一个线程将访问该堆栈,因此额外的代码将不执行。
但是要注意,现在你要负责保证堆栈对线程的安全性。
系统将不对此负责。
18.2.5迅速释放堆栈
最后要说明的是,将专用堆栈用于某些数据结构后,就可以释放整个堆栈,而不必显式释放堆栈中的每个内存块。
例如,当WindowsExplorer遍历硬盘驱动器的目录层次结构时,它必须在内存中建立一个树状结构。
如果你告诉WindowsExplorer刷新它的显示器,它只需要撤消包含这个树状结构的堆栈并且重新运行即可(当然,假定它将专用堆栈用于存放目录树信息)。
对于许多应用程序来说,这是非常方便的,并且它们也能更快地运行。
18.3如何创建辅助堆栈
你可以在进程中创建辅助堆栈,方法是让线程调用HeapCreate函数:
HANDLEHeapCreate(
DWORDfdwOptions,
SIZE_TdwInitialSize,
SIZE_TdwMaximumSize);
第一个参数fdwOptions用于修改如何在堆栈上执行各种操作。
你可以设定0、HEAP_NO_SERIALIZE、HEAP_GENERATE_EXCEPTIONS或者是这两个标志的组合。
按照默认设置,堆栈将顺序访问它自己,这样,多个线程就能够分配和释放堆栈中的内存块而不至于破坏堆栈。
当试图从堆栈分配一个内存块时,HeapAlloc函数(下面将要介绍)必须执行下列操作:
1)遍历分配的和释放的内存块的链接表。
2)寻找一个空闲内存块的地址。
3)通过将空闲内存块标记为“已分配”分配新内存块。
4)将新内存块添加给内存块链接表。
下面这个例子说明为什么应该避免使用HEAP_NO_SERIALIZE标志。
假定有两个线程试图同时从同一个堆栈中分配内存块。
线程1执行上面的第一步和第二步,获得了空闲内存块的地址。
但是,在该线程可以执行第三步之前,它的运行被线程2抢占,线程2得到一个机会来执行上面的第一步和第二步。
由于线程1尚未执行第三步,因此线程2发现了同一个空闲内存块的地址。
由于这两个线程都发现了堆栈中它们认为是空闲的内存块,因此线程1更新了链接表,给新内存块做上了“已分配”的标记。
然后线程2也更新了链接表,给同一个内存块做上了“已分配”标记。
到现在为止,两个线程都没有发现问题,但是两个线程得到的是完全相同的内存块的地址。
这种类型的错误是很难跟踪的,因为它不会立即表现出来。
相反,这个错误会在后台等待着,直到很不适合的时候才显示出来。
可能出现的问题是:
•内存块的链接表已经被破坏。
在试图分配或释放内存块之前,这个问题不会被发现。
•两个线程共享同一个内存块。
线程1和线程2会将信息写入同一个内存块。
当线程1查看该内存块的内容时,它将无法识别线程2提供的数据。
•一个线程可能继续使用该内存块并且将它释放,导致另一个线程改写未分配的内存。
这将破坏该堆栈。
解决这个问题的办法是让单个线程独占对堆栈和它的链接表的访问权,直到该线程执行了对堆栈的全部必要的操作。
如果不使用HEAP_NO_SERIALIZE标志,就能够达到这个目的。
只有当你的进程具备下面的一个或多个条件时,才能安全地使用HEAP_NO_SERIALIZE标志:
•你的进程只使用一个线程。
•你的进程使用多个线程,但是只有单个线程访问该堆栈。
•你的进程使用多个线程,但是它设法使用其他形式的互斥机制,如关键代码段、互斥对象和信标(第8、9章中介绍),以便设法自己访问堆栈。
如果对是否可以使用HEAP_NO_SERIALIZE标志没有把握,那么请不要使用它。
如果不使用该标志,每当调用堆栈函数时,线程的运行速度会受到一定的影响,但是不会破坏你的堆栈及其数据。
另一个标志HEAP_GENERATE_EXCEPTIONS,会在分配或重新分配堆栈中的内存块的尝试失败时,导致系统引发一个异常条件。
所谓异常条件,只不过是系统使用的另一种方法,以便将已经出现错误的情况通知你的应用程序。
有时在设计应用程序时让它查看异常条件比查看返回值要更加容易些。
异常条件将在第23、24和25章中介绍。
HeapCreate的第二个参数dwInitialSize用于指明最初提交给堆栈的字节数。
如果必要的话,HeapCreate函数会将这个值圆整为CPU页面大小的倍数。
最后一个参数dwMaximumSize用于指明堆栈能够扩展到的最大值(即系统能够为堆栈保留的地址空间的最大数量)。
如果dwMaximumSize大于0,那么你创建的堆栈将具有最大值。
如果尝试分配的内存块会导致堆栈超过其最大值,那么这种尝试就会失败。
如果dwMaximumSize的值是0,那么可以创建一个能够扩展的堆栈,它没有内在的限制。
从堆栈中分配内存块只需要使堆栈不断扩展,直到物理存储器用完为止。
如果堆栈创建成功,HeapCreate函数返回一个句柄以标识新堆栈。
该句柄可以被其他堆栈函数使用。
18.3.1从堆栈中分配内存块
若要从堆栈中分配内存块,只需要调用HeapAlloc函数:
PVOIDHeapAlloc(
HANDLEhHeap,
DWORDfdwFlags,
SIZE_TdwBytes);
第一个参数hHeap用于标识分配的内存块来自的堆栈的句柄。
dwBytes参数用于设定从堆栈中分配的内存块的字节数。
参数fdwFlags用于设定影响分配的各个标志。
目前支持的标志只有3个,即HEAP_ZERO_MEMORY、HEAP_GENERATE_EXCEPTIONS和HEAP_NO_SERIALIZE。
HEAP_ZERO_MEMORY标志的作用应该是非常清楚的。
该标志使得HeapAlloc在返回前用0来填写内存块的内容。
第二个标志HEAP_GENERATE_EXCEPTIONS用于在堆栈中没有足够的内存来满足需求时使HeapAlloc函数引发一个软件异常条件。
当用HeapCreate函数创建堆栈时,可以设定HEAP_GENERATE_EXCEPTIONS标志,它告诉堆栈,当不能分配内存块时,就应该引发一个异常条件。
如果在调用HeapCreate函数时设定了这个标志,那么当调用HeapAlloc函数时,就不需要设定该标志。
另外,你可能想要不使用该标志来创建堆栈。
在这种情况下,为HeapAlloc函数设定该标志只会影响对HeapAlloc函数的一次调用,并不是每次调用都会受到影响。
如果HeapAlloc运行失败,引发一个异常条件,那么这个异常条件将是表18-1中的两个异常条件之一。
表18-1异常条件
标志
含义
STATUS_NO_MEMORY
由于内存不够,分配内存块的尝试失败
STATUS_ACCESS_VIOLATION
由于堆栈被破坏,或者函数的参数不正确,分配内存块的尝试失败
如果内存块已经成功地分配,HeapAlloc返回内存块的地址。
如果内存不能分配并且没有设定HEAP_GENERATE_EXCEPTIONS标志,那么HeapAlloc函数返回NULL。
最后一个标志HEAP_NO_SERIALIZE可以用来强制对HeapAlloc函数的调用与访问同一个堆栈的其他线程不按照顺序进行。
在使用这个标志时应该格外小心,因为如果其他线程在同一时间使用该堆栈,那么堆栈就会被破坏。
当从你的进程的默认堆栈中分配内存块时,决不要使用这个标志,因为数据可能被破坏,你的进程中的其他线程可能在同一时间访问默认堆栈。
Windows98如果调用HeapAlloc函数并且要求分配大于256MB的内存块,Windows98就将它看成是一个错误,函数的调用将失败。
注意,在这种情况下,该函数总是返回NULL,并且不会引发异常条件,即使你在创建堆栈或者试图分配内存块时使用HEAP_GENERATE_EXCEPTIONS标志,也不会引发异常条件。
注意当你分配较大的内存块(大约1MB或者更大)时,最好使用VirtualAlloc函数,应该避免使用堆栈函数。
18.3.2改变内存块的大小
常常需要改变内存块的大小。
有些应用程序开始时分配的内存块比较大,然后,当所有数据放入内存块后,再缩小内存块的大小。
有些应用程序开始时分配的内存块比较小,后来需要将更多的数据拷贝到内存块中去时,再设法扩大它的大小。
如果要改变内存块的大小,可以调用HeapReAlloc函数:
PVOIDHeapReAlloc(
HANDLEhHeap,
DWORDfdwFlags,
PVOIDpvMem,
SIZE_TdwBytes);
与其他情况一样,hHeap参数用于指明包含你要改变其大小的内存块的堆栈。
fdwFlags参数用于设定改变内存块大小时HeapReAlloc函数应该使用的标志。
可以使用的标志只有下面4个,即HEAP_GENERATE_EXCEPTIONS、HEAP_NO_SERIALIZE、HEAP_ZERO_MEMORY和HEAP_REALLOC_IN_PLACE_ONLY。
前面两个标志在用于HeapAlloc时,其作用相同。
HEAP_ZERO_MEMORY标志只有在你扩大内存块时才使用。
在这种情况下,内存块中增加的字节将被置0。
如果内存块已经被缩小,那么该标志不起作用。
HEAP_REALLOC_IN_PLACE_ONLY标志告诉HeapReAlloc函数,它不能移动堆栈中的内存块。
如果内存块在增大,HeapReAlloc函数可能试图移动内存块。
如果HeapReAlloc能够扩大内存块而不移动它,那么它将会这样做并且返回内存块的原始地址。
另外,如果HeapReAlloc必须移动内存块的内容,则返回新的较大内存块的地址。
如果内存块被缩小,HeapReAlloc将返回内存块的原始地址。
如果内存块是链接表或二进制树的组成部分,那么可以设定HEAP_REALLOC_IN_PLACE_ONLY标志。
在这种情况下,链接表或二进制树中的其他节点可能拥有该节点的指针,改变堆栈中的节点位置会破坏链接表的完整性。
其余的两个参数pvMem和dwBytes用于设定你要改变其大小的内存块的地址和内存块的新的大小(以字节为计量单位)。
HeapReAlloc既可以返回新的改变了大小的内存块的地址,也可以在内存块不能改变大小时返回NULL。
18.3.3了解内存块的大小
当内存块分配后,可以调用HeapSize函数来检索内存块的实际大小:
SIZE_THeapSize(
HANDLEhHeap,
DWORDfdwFlags,
LPCVOIDpvMem);
参数hHeap用于标识堆栈,参数pvMem用于指明内存块的地址。
参数fdwFlags既可以是0,也可以是HEAP_NO_SERIALIZE。
18.3.4释放内存块
当不再需要内存块时,可以调用HeapFree函数将它释放:
BOOLHeapFree(
HANDLEhHeap,
DWORDfdwFlags,
PVOIDpvMem);
HeapFree函数用于释放内存块,如果它运行成功,便返回TRUE。
参数fdwFlags既可以是0,也可以是HEAP_NO_SERIALIZE。
调用这个函数可使堆栈管理器收回某些物理存储器,但是这没有保证。
18.3.5撤消堆栈
如果应用程序不再需要它创建的堆栈,可以通过调用HeapDestroy函数将它撤消:
BOOLHeapDestroy(HANDLEhHeap);
调用HeapDestroy函数可以释放堆栈中包含的所有内存块,也可以将堆栈占用的物理存储器和保留的地址空间区域重新返回给系统。
如果该函数运行成功,HeapDestroy返回TRUE。
如果在进程终止运行之前没有显式撤消堆栈,那么系统将为你将它撤消。
但是,只有当进程终止运行时,堆栈才能被撤消。
如果线程创建了一个堆栈,当线程终止运行时,该堆栈将不会被撤消。
在进程完全终止运行之前,系统不允许进程的默认堆栈被撤消。
如果将进程的默认堆栈的句柄传递给HeapDestroy函数,系统将忽略对该函数的调用。
18.3.6用C++程序来使用堆栈
使用堆栈的最好方法之一是将堆栈纳入现有的C++程序。
在C++中,调用new操作符,而不是调用通常的C运行期例程malloc,就可以执行类对象的分配操作。
然后,当我们不再需要这个类对象时,调用delete操作符,而不是调用通常的C运行期例程free将它释放。
例如,我们有一个称为CSomeClass的类,我们想要分配这个类的一个实例,那么可以使用类似下面的句法:
CSomeClass*pSomeClass=newCSomeClass;
当C++编译器查看这一行代码时,它首先查看CSomeClass类是否包含new操作符的成员函数。
如果包含,那么编译器就生成调用该函数的代码。
如果编译器没有找到重载new操作符的函数,那么编译器将生成调用标准C++的new操作符函数的代码。
当完成对
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 18 堆栈