Windows核心编程 第十八章 堆栈

1 8章 堆 栈

    对内存进行操作的第三个机制是使用堆栈。堆栈可以用来分配许多较小的数据块。例如,若要对链接表和链接树进行管理,最好的方法是使用堆栈,而不是第 1 5章介绍的虚拟内存操作方法或第1 7章介绍的内存映射文件操作方法。堆栈的优点是,可以不考虑分配粒度和页面边界之类的问题,集中精力处理手头的任务。堆栈的缺点是,分配和释放内存块的速度比其他机制要慢,并且无法直接控制物理存储器的提交和回收。

从内部来讲,堆栈是保留的地址空间的一个区域。开始时,保留区域中的大多数页面没有被提交物理存储器。当从堆栈中进行越来越多的内存分配时,堆栈管理器将把更多的物理存储器提交给堆栈。物理存储器总是从系统的页文件中分配的,当释放堆栈中的内存块时,堆栈管理器将收回这些物理存储器。

18.1 进程的默认堆栈

    当进程初始化时,系统在进程的地址空间中创建一个堆栈。该堆栈称为进程的默认堆栈。

按照默认设置,该堆栈的地址空间区域的大小是 1 MB。但是,系统可以扩大进程的默认堆栈,使它大于其默认值。当创建应用程序时,可以使用 / H E A P链接开关,改变堆栈的1 M B默认区域大小。由于 D L L没有与其相关的堆栈,所以当链接 D L L时,不应该使用 / H E A P链接开关。

/ H E A P链接开关的句法如下:

/ H E A P:reserve[,commit]

许多Wi n d o w s函数要求进程使用其默认堆栈。例如, Windows 2000的核心函数均使用U n i c o d e字符和字符串执行它们的全部操作。如果调用Wi n d o w s函数的A N S I版本,那么该A N S I版本必须将A N S I字符串转换成U n i c o d e字符串,然后调用同一个函数的 U n i c o d e版本。为了进行字符串的转换,A N S I函数必须分配一个内存块,以便放置U n i c o d e版本的字符串。该内存块是从你的进程的默认堆栈中分配的。 Wi n d o w s的其他许多函数需要使用一些临时内存块,这些内存块是从进程的默认堆栈中分配的。

由于进程的默认堆栈可供许多Wi n d o w s函数使用,你的应用程序有许多线程同时调用各种Wi n d o w s函数,因此对默认堆栈的访问是顺序进行的。

单个进程可以同时拥有若干个堆栈。这些堆栈可以在进程的寿命期中创建和撤消。但是,

默认堆栈是在进程开始执行之前创建的,并且在进程终止运行时自动被撤消。不能撤消进程的默认堆栈。每个堆栈均用它自己的堆栈句柄来标识,用于分配和释放堆栈中的内存块的所有堆栈函数都需要这个堆栈句柄作为其参数。

可以通过调用G e t P r o c e s s H e a p函数获取你的进程默认堆栈的句柄:

HANDLE GetProcessHeap();


18.2 为什么要创建辅助堆栈

除了进程的默认堆栈外,可以在进程的地址空间中创建一些辅助堆栈。由于下列原因,你可能想要在自己的应用程序中创建一些辅助堆栈:

• 保护组件。

• 更加有效地进行内存管理。

• 进行本地访问。

• 减少线程同步的开销。

• 迅速释放。

下面让我们来详细说明每个原因。

18.2.1 保护组件

    假如你的应用程序需要保护两个组件,一个是节点结构的链接表,一个是 B R A N C H结构的二进制树。你有两个源代码文件,一个是 L n k L s t . c p p,它包含负责处理

N O D E链接表的各个函数,另一个文件是B i n Tr e e . c p p,它包含负责处理

分支的二进制树的各个函数。

    如果节点和分支一道存储在单个堆栈中,那么这个组合堆栈将类似图1 8 - 1所示的样子。

    现在假设链接表代码中有一个错误,它使节点 1后面的8个字节不小心被改写了,从而导致分支 3中的数据被破坏。当B i n Tr e e . c p p文件中的代码后来试图遍历二进制树时,它将无法进行这项操作,因为它的内存已经被破坏。当然,这使你认为二进制树代码中存在一个错误,而实际上错误是在链接表代码中。由于不同类型的对象混合放在单个堆栈中,因此跟踪和确定错误将变得非常困难。

    通过创建两个独立的堆栈,一个堆栈用于存放节点,另一个堆栈用于存放分支,就能够确定你的问题。你的链接表代码中的一个小错误不会破坏你的二进制树的完整性。反过来,二进制树中的小错误也不会影响链接表代码中的数据完整性。但是,你的代码中的错误仍然

可能导致对堆栈进行杂乱的内存写操作,不过出现这种情况的可能性很小。

18.2.2 更有效的内存管理

    通过在堆栈中分配同样大小的对象,就可以更加有效地管理堆栈。例如,假设每个节点结构需要2 4字节,每个分支结构需要3 2字节。所有这些对象均从单个堆栈中分配。图 1 8 - 2显示了单个堆栈中已经分配的若干个节点和分支对象占满了这个堆栈。如果节点 2和节点4被释放,堆栈中的内存将变成许多碎片。这时,如果试图分配分支结构,那么尽管分支只需要3 2个字节,而实际上可以使用的有4 8个字节,但是分配仍将失败。

如果每个堆栈只包含大小相同的对象,那么释放一个对象后,另一个对象就可以恰好放入被释放的对象空间中。


18.2.3 进行本地访问

    每当系统必须在R A M与系统的页文件之间进行 R A M页面的交换时,系统的运行性能就会受到很大的影响。如果经常访问局限于一个小范围地址的内存,那么系统就不太可能需要在 R A M与磁盘之间进行页面的交换。

    所以,在设计应用程序的时候,如果有些数据将被同时访问,那么最好把它们分配在互相靠近的位置上。让我们回到链接表和二进制树的例子上来,遍历链接表与遍历二进制树之间并无什么关系。如果将所有的节点放在一起(放在一个堆栈中),就可以使这些节点位于相邻的页面上。实际上,若干个节点很可能恰好放入单个物理内存页面上。遍历链接表将不需要 C P U为了访问每个节点而引用若干不同的内存页面。

如果将节点和分支分配在单个页面上,那么节点就不一定会互相靠在一起。在最坏的情况下,每个内存页面上可能只有一个节点,而其余的每个页面则由分支占用。在这种情况下,遍历链接表将可能导致每个节点的页面出错,从而使进程运行得极慢。

18.2.4 减少线程同步的开销

    正如下面就要介绍的那样,按照默认设置,堆栈是顺序运行的,这样,如果多个线程试图同时访问堆栈,就不会使数据受到破坏。但是,堆栈函数必须执行额外的代码,以保证堆栈对线程的安全性。如果要进行大量的堆栈分配操作,那么执行这些额外的代码会增加很大的负担,从而降低你的应用程序的运行性能。当你创建一个新堆栈时,可以告诉系统,只有一个线程将访问该堆栈,因此额外的代码将不执行。但是要注意,现在你要负责保证堆栈对线程的安全性。系统将不对此负责。

18.2.5 迅速释放堆栈

    最后要说明的是,将专用堆栈用于某些数据结构后,就可以释放整个堆栈,而不必显式释放堆栈中的每个内存块。例如,当Windows Explorer遍历硬盘驱动器的目录层次结构时,它必须在内存中建立一个树状结构。如果你告诉 Windows Explorer刷新它的显示器,它只需要撤消包含这个树状结构的堆栈并且重新运行即可(当然,假定它将专用堆栈用于存放目录树信息)。对于许多应用程序来说,这是非常方便的,并且它们也能更快地运行。

 

之后的内容就是介绍一些函数,这里我就把函数写出来,具体使用细节可以查看文档。

1.创建辅助堆栈
HANDLE HeapCreate(
DWORD fdwOptions,
SIZE_T dwInitialSize,
SIZE_T dwMaximumSize);
2.从堆栈中分配内存块
PVOID HeapAlloc(
HANDLE hHeap,
DWORD fdwFlags,
SIZE_T dwButes);
3.改变内存块大小
PVOID HeapReAlloc(
HANDLE hHeap,
DWORD fdwFlags,
PVOID pvMem,
SIZE_T dwBytes);
4.了解内存块大小
SIZE_T HeapSize(
HANDLE hHeap,
DWORD fdwFlags,
LPCVOID pvMem);
5.释放内存块
BOOL HeapFree(
HANDLE hHeap,
DWORD fdwFlags,
PVOID pvMem);
6.撤销堆栈
BOOL HeapDestroy(HANDLE hHeap);
7.获取现有堆栈信息
DWPRD GetProcessHeaps(
DWORD dwNumHeaps,
PNANDLE pHeaps);
8.验证堆栈完整性
BOOL HeapVa;odate(
HANDLE hHeap,
DWORD fdwFlags,
LPCVOID pvMem);
9.合并地址中的空闲内存块回收不包含已经分配的地址内存块的存储器页面
UINT HeapCompact(
HANDLE hHeap,
DWORD fdwFlags);
 

10.堆栈上锁
BOOL HeapLock(HANDLE hHeap);
BOOL HeapUnLock(HANDLE hHeap);
11.遍历堆栈内容
BOOL HeapWalk(
HANDLE hHeap,
PPROCESS_HEAP_ENTRY pHeapEntry);



原文地址:https://www.cnblogs.com/csnd/p/12062112.html