前言
windows 对内存进行操作的机制:
- 虚拟内存
- 文件映射
- 堆栈
使用堆栈场景:
- 堆栈可以用来分配许多较小的数据块。
例如,若要对链接表和链接树进行管理,最好的方法是使用堆栈。(而不是第15章介绍的虚拟内存操作方法或第1 7章介绍的内存映射文件操作方法)
堆栈优缺点:
- 堆栈的优点是,
可以不考虑分配粒度和页面边界之类的问题,集中精力处理手头的任务。 - 堆栈的缺点是,
1.分配和释放内存块的速度比其他机制要慢。
2.并且无法直接控制物理存储器的提交和回收。
堆栈是什么:
堆栈实际是一种栈,Push Pop FIFO特性。
只不过这个操作区域都是在进程的栈区。
请见总结。
特点1
内存分配达到一定页面数量时才会提交物理分配器,堆栈管理器才会提交到堆栈,堆栈释放时也是通过堆栈管理器回收已分配得物理存储器。
从内部来讲,堆栈是地址空间的一个保留区域。
开始时,该保留区域中的大多数页面没有被提交物理存储器。当从堆栈中进行越来越多的内存分配时,堆栈管理器将把更多的物理存储器提交给堆栈。物理存储器总是从系统的页文件中分配的,当释放堆栈中的内存块时,堆栈管理器将收回这些物理存储器。
Microsoft并没有以文档的形式来规定堆栈释放和收回存储器时应该遵循的具体规则, Windows 98 与Windows 2000的规则是不同的。可以这样说,Windows 98 更加注重内存的使用, 因此只要可能,它就收回堆栈。Windows 2000 更加注重速度,因此它往往较长时间占用物理存储器,只有在一段时间后页面不再使用时,才将它返回给页文件。 Microsoft常常进行适应性测 试并运行各种不同的条件,以确定在大部分时间内最适合的规则。随着使用这些规则的应用程序和硬件的变更,这些规则也会有所变化。如果了解这些规则对你的应用程序非常关键,那么 请不要使用堆栈。相反,可以使用虚拟内存函数(即 VirtualAlloc和VirtualFree),这样,就能够控制这些规则。
一、进程的默认堆栈
进程的默认堆栈:当进程初始化时,系统在进程的地址空间中创建一个堆栈。该堆栈称为进程的默认堆栈。按照默认设置,该堆栈的地址空间区域的大小是 1 MB。
但是,系统可以扩大进程的默认堆栈,使它大于其默认值。当创建应用程序时,可以使用 / H E A P链接开关,改变堆栈的1 M B默认区域大小。由于 D L L没有与其相关的堆栈,所以当链接 D L L时,不应该使用 /HEAP链接开关。
/ HEAP链接开关的句法如下:
/HEAP: reserve[ .commit]
许多Windows函数要求进程使用其默认堆栈。例如, Windows 2000的核心函数均使用
Unicode字符和字符串执行它们的全部操作。如果调用 Windows函数的A N S I版本,那么该ANSI版本必须将ANSII字符串转换成Unicode字符串,然后调用同一个函数的 Unicode版本。为了进行字符串的转换,A N S I函数必须分配一个内存块,以便放置 Unicode版本的字符串。该内存块是从你的进程的默认堆栈中分配的。 Windows的其他许多函数需要使用一些临时内存块,这些内存块是从进程的默认堆栈中分配的。另外,老的 1 6位Windows函数LocalAlloc和GlobalAlloc也是从进程的默认堆栈中进行它们的内存分配的。
特点1
对默认堆栈的访问是顺序进行的。
系统必须保证在规定的时间内,每次只有一个线程能够分配和释放默认堆栈中的内存块。如果两个线程试图同时分配默认堆栈中的内存块,那么只有一个线程能够分配内存块,另一个线程必须等待第一个线程的内存块分配之后,才能分配它的内存块。一旦第一个线程的内存块分配完,堆栈函数将允许第二个线程分配内存块。
缺点:
- 这种顺序访问方法对速度有一定的影响。
特点2
单个进程可以同时拥有若干个堆栈。
这些堆栈可以在进程的寿命期中创建和撤消。但是,默认堆栈是在进程开始执行之前创建的,并且在进程终止运行时自动被撤消。不能撤消进程的默认堆栈。每个堆栈均用它自己的堆栈句柄来标识,用于分配和释放堆栈中的内存块的所有堆栈函数都需要这个堆栈句柄作为其参数。
可以通过调用G e t P r o c e s s H e a p函数获取你的进程默认堆栈的句柄:
HANDLE GetProcessHeap( );
二、为什么要创建辅助堆栈
除了进程的默认堆栈外,可以在进程的地址空间中创建一些辅助堆栈。
由于下列原因,你可能想要在自己的应用程序中创建一些辅助堆栈:
- 保护组件。
- 更加有效地进行内存管理。
- 进行本地访问。
- 减少线程同步的开销。
- 迅速释放。
2.1 保护组件
需要保护的组件都放在同一个堆栈的话,某一个保护组件的出错会影响另一个组件。
所以有必要创建两个堆栈分别存放需要保护的组件,进行隔离保护。
2.2 更有效的内存管理
需要保护的组件都放在同一个堆栈的话,某一个保护组件的部分对象被释放时,再放入另一个保护组件的对象,因为两种对象的大小不同,导致会产生内存碎片。
反之,如果每个堆栈只包含大小相同的对象,那么释放一个对象后,另一个对象就可以恰好放入被释放的对象空间中。
2.3 进行本地访问
每当系统必须在RAM与系统的页文件之间进行RAM页面的交换时,系统的运行性能就会受到很大的影响。
如果经常访问局限于一个小范围地址的内存,那么系统就不太可能需要在RAM与磁盘之间进行页面的交换。
所以,在设计应用程序的时候,如果有些数据将被同时访问,那么最好把它们分配在互相
靠近的位置上。
需要保护的组件都放在同一个堆栈的话,遍历某一个组件的所有对象时,可能存在组件的对象存放在不同内存页面,最坏情况下就是一个组件的对象存放在一个内存页面,其余都是其他组件的对象,在这种情况下,遍历目标组件对象,将可能导致每个节点的页面出错(即页文件不存在对象,需要重新映射页文件),从而使进程运行得极慢,耗时。
2.4 减少线程同步的开销
多线程访问同一个堆栈时,由于堆栈访问时顺序的,系统会对访问进行顺序控制,因此数据不会受到破坏。但是存在缺点:
- 堆栈函数必须要执行额外代码。用以保证堆栈对线程的安全性,如果要进行大量的堆栈分配操作,那么执行这些额外的代码会增加很大的负担,从而降低你的应用程序的运行性能。
反之,
一个线程只访问一个堆栈时,可以告诉系统 堆栈函数不用执行额外代码,但是堆栈对线程的安全性却需要自己管理,系统不负责了。
2.5 迅速释放堆栈
最后要说明的是,将专用堆栈用于某些数据结构后,就可以释放整个堆栈,而不必显式释
放堆栈中的每个内存块。
三、如何创建辅助堆栈
你可以在进程中创建辅助堆栈,方法是让线程调用 HeapCreate函数:
HANDLE HeapCreate( DWORD fdwOptions, SIZE_T dwInitia1Size. SIZE_T dwMaximumSize ); .
fdwOptions:修改如何在堆栈上执行各种操作,你可以设定 0、HEAP_NO_SERIALIZE、HEAP_GENERATE_EXCEPTIONS 或者是这两个标志的组合。
dwInitialSize:用于指明最初提交给堆栈的字节数。如果必要的话,HeapCreate 函数会将这个值转整为CPU页面大小的倍数。
dwMaximumSize:用于指明堆栈能够扩展到的最大值(即系统能够为堆栈保留的地址空间的最大数量)。
如果dwMaximumSize 大于0,那么你创建的堆栈将具有最大值。
如果尝试分配的内存块会导致堆栈超过其最大值,那么这种尝试就会失败。
如果dwMaximumSize的值是0,那么可以创建一个能够扩展的堆栈,它没有内在的限制。从堆栈中分配内存块只需要使堆栈不断扩展,直到物理存储器用完为止。
如果堆栈创建成功,HeapCreate函数返回一个句柄以标识新堆栈。该句柄可以被其他堆栈函数使用。
按照默认设置,堆栈将顺序访问它自己,这样,多个线程就能够分配和释放堆栈中的内存 块而不至于破坏堆栈。
当试图从堆栈分配一个内存块时, HeapAlloc函数(下面将要介绍)必须执行下列操作:
- 遍历分配的和释放的内存块的链接表。
- 寻找一个空闲内存块的地址。
- 通过将空闲内存块标记为“已分配”分配新内存块。
- 将新内存块添加给内存块链接表。
3.0 简述标志作用
HEAP_NO_SERIALIZE 标志
应该避免使用 HEAP_NO_SERIALIZE标志:
假定有两个线程试图同时从同一个堆栈中分配内存块。线程 1执行上面的第一步和第二步,获得了空闲内存块的地址。但是,在该线程可以执行第三步之前,它的运行被线程 2抢占,线程 2得到一个机会来执行上面的第一步和第二步。由于线程 1尚未执行第三步,因此线程2发现了同一个空闲内存块的地址。
由于这两个线程都发现了堆栈中它们认为是空闲的内存块,因此线程 1更新了链接表,给新内存块做上了“已分配”的标记。然后线程 2也更新了链接表,给同一个内存块做上了“已分配”标记。到现在为止,两个线程都没有发现问题,但是两个线程得到的是完全相同的内存块的地址。
总结就是,由于线程之间的抢占机制,可能导致不同的线程申请了同一块内存块,引发出一些问题。可能出现的问题是:
- 内存块的链接表已经被破坏。在试图分配或释放内存块之前,这个问题不会被发现。
- 两个线程共享同一个内存块。线程1和线程2会将信息写入同一个内存块。当线程1查看该
内存块的内容时,它将无法识别线程2提供的数据。 - 一个线程可能继续使用该内存块并且将它释放,导致另一个线程改写未分配的内存。这
将破坏该堆栈。
解决以上问题的方法是:
- 单个线程独占对堆栈和它的链接表的访问权,直到该线程执行了 对堆栈的全部必要的操作。
- 不使用 H E A P _ N O _ S E R I A L I Z E标志,就能够达到这个目的。
安全地使用 HEAP_NO_SERIALIZE标志的条件:
- 你的进程只使用一个线程。
- 你的进程使用多个线程,但是只有单个线程访问该堆栈。
- 你的进程使用多个线程,但是它设法使用其他形式的互斥机制,如关键代码段、互斥对
象和信标(第8、9章中介绍),以便设法自己访问堆栈。
不使用 HEAP_NO_SERIALIZE 标志的优缺点:(推荐不使用!)
- 缺点:不使用的话,每当调用堆栈函数时,线程的运行速度会受到一定的影响。
- 优点:不会破坏你的堆栈及其数据。
HEAP_GENERATE_EXCEPTIONS 标志
会在分配或重新分配堆栈中的内存块的尝试失败时,导致系统引发一个异常条件。
所谓异常条件,只不过是系统使用的另一种方法,以便将已经出现错误的情况通知你的应用程序。有时在设计应用程序时让它查看异常条件比查看返回值要更加容易些。