来自Windows核心编程 – 18章

使用堆是一种对内存进行操控的方法,另外两种是虚拟内存 和 内存映射文件。

堆非常适合用来分配小型数据。也是用来管理链表和树的最佳方式。

堆的缺点是 分配 和 释放 内存块的速度比其他方式慢。

无法再对物理存储器的调拨和(撤销调拨)释放进行控制。

在系统内部,堆就是一块预定的地址空间区域。

刚开始,区域内的大部分页面没有调拨物理存储器,随着我们不断从堆中分配内存,堆的管理器会给堆调拨越来越多的物理存储器。这些物理存储器始终是从页交换文件中分配的。释放堆中的内存块时,堆管理器你会撤销(释放)已调拨的物理存储器。

18.1 进程的默认堆

进程初始化的时候,系统会在进程的地址空间中创建一个堆。这个堆被称为进程的默认堆。默认情况下,这个堆的大小是 1MB。但是系统可以增大进程的默认堆。

系统保证不管在什么时候,只运行一个线程从堆中分配或释放块。

我们可以通过调用GetProcessHeap() 函数来得到进程的默认堆的句柄。

 

18.2 为什么要创建额外的堆

除了使用进程的默认堆之外,我们还可以创建额外的堆。

创建额外的堆有什么好处呢:

  1. 对组件进行保护
  2. 更有效的内存管理
  3. 局部访问
  4. 避免线程同步的开销
  5. 快速释放

一个个来看吧:

对组建的保护:

简单的来讲就是内存越界,在文件1中数据结构1有缺陷,内存越界修改了文件2中某个数据结构的数据,导致了错误,而我们认为这个错误来自于文件2…完蛋,这个BUG就很难找了…

更有效的内存管理:

例子:数据结构1-占24字节,数据结构2-占32字节。

然后我们数据结构1 和 2交替填充了整个堆,释放了所有的数据结构1…造成了大量内存碎片,而且,此时除非扩充堆,我们无法为数据结构2分配哪怕仅仅一个空间!

使内存访问局部化:

当系统把一个内存页面换出到页交换文件,或者把页交换文件中的一个页面换入到内存中的时候,会对性能产生非常大的影响。

比较好的做法是将需要同时访问的对象分配在相邻的内存地址。

否则最坏的情况是访问每个元素(比如链表)都会引起页面错误,导致整个过程及其缓慢。

避免线程的同步开销:

默认情况下,对堆的访问是依次进行的,此时堆函数会执行额外的代码来确保堆的线程安全性(thread-safe)。但是如果创建一个新的堆,就可以告诉系统只有一个线程会访问堆,然后由我们程序员手动控制来保证堆的线程安全性。

快速释放:

如果我们仅将一个树或者链表存储到一个堆里面,释放树或者链表的时候,我们不必显式地释放堆中每个内存块,可以直接释放整个堆,是不是很快…

 

18.3 如何创建额外的堆

调用 HeapCreate() 函数可以创建额外的堆。(属于进程)

三个参数:

  1. fdwOptions 用来表示对堆的操作该如何进行。可以指定 0、HEAP_NO_SERIALIZE、HEAP_GENERATE_EXCEPTIONS、HEAP_CREATE_ENABLE_EXECUTE 或这些标志的组合。
  2. 第二个参数 dwInitialSize 表示一开始要调拨给堆的字节数。如果需要,会把这个值向上取整到CPU页面大小的整数倍。
  3. 最后一个参数 dwMaximumSize 表示堆所能增长的最大大小(即系统为堆所预定的地址空间的大小)。如果大于0,则表示有一个最大大小,分配超过这个值,则可能失败。如果等于0,那么创建的堆是可增长的,没有上线,直到用尽物理存储器为止。

创建成功后会返回一个句柄,标识创建的堆,其他堆函数会用到这个句柄。

接下来我们详细讨论下第一个参数的值。

HEAP_NO_SERIALIZE : 尽量不要用这个值,除非你能自己管理访问次序。

HEAP_GENERATE_EXCEPTIONS:这个标志告诉系统,每当在堆内分配或重新分配内存失败的时候,抛出一个异常。异常只不过是系统使用的另一种方法,来告诉应用程序有错误发生。

HEAP_CREATE_ENABLE_EXECUTE:如果想在堆中存放可执行代码,就需要设置这个标志。如果不设置这个标志的话,当我们试图在 这个堆的内存块中 执行代码时,系统会抛出 EXCEPTION_ACCESS_VIOLATION 异常。

 

18.3.1 从堆中分配内存块

从堆中分配一块内存不过是调用下 HeapAlloc() 函数

三个参数:

  1.  hHeap 是一个堆的句柄。
  2.  fdwFlags 用来指定标识,可以指定的有: HEAP_ZERO_MEMORY、HEAP_GENERATE_EXCEPTIONS 和 HEAP_NO_SERIALIZE。
  3. 第三个参数用来指定分配的大小。单位为字节。

同上我们来讨论下第二个参数的三个标识:

HEAP_ZERO_MEMORY :让 HeapAlloc 在返回之前把内存块的内容都清零。

HEAP_GENERATE_EXCEPTIONS :告诉系统,如果分配失败,就抛出异常。

在创建的时候也可以使用这个标识,如果当时指定了,这次就不需要了。

其实最好是创建堆的时候不指定,这样我们可以控制需要的时候抛出。

如果抛出异常应该是以下之一:

堆分配异常

如果分配成功,返回地址。

如果分配失败,没有指定异常标志,返回NULL

HEAP_NO_SERIALIZE:强制系统不要把这次HeapAlloc调用与其他线程对同一个堆的访问一次排列起来。在使用这个标识的时候应该极其小心,因为如果其他线程也在分配堆,可能会破坏堆。

在从进程默认堆分配内存的时候,绝对不要使用这个标志。

PS.在分配1MB以上内存的时候,建议使用VirtualAlloc函数(虚拟内存)。

 

18.3.2 调整内存块大小

我们有些时候可能会调整内存块大小,比如应用程序一开始会分配一块较大的内存,经过一段时间使用后,可能需要减少内存块的大小。或者一开始分配的比较小,然后需要更大的空间,都可以通过这个函数来完成。

四个参数:

  1.  hHeap 标识堆
  2.  fdwFlag 这次有四个标识:HEAP_ZERO_MEMORY、HEAP_GENERATE_EXCEPTIONS 和 HEAP_NO_SERIALIZE 和 HEAP_REALLOC_IN_PLACE_ONLY。
  3.  pvMem 用来指定想要调整大小的内存块的当前地址。
  4.  dwByte 用来指定新的大小。

接下来我们先讲标识符。

HEAP_NO_SERIALIZE 和 HEAP_GENERATE_EXCEPTIONS 意思和分配的时候一样。

HEAP_ZERO_MEMORY 标识新空间要不要清零

HEAP_REALLOC_IN_PLACE_ONLY :在增大内存块的时候,HeapReAlloc可能会在堆内部移动内存块,而这个标识符则表示不要移动内存块。如果能够在不移动内存块的前提下完成操作,则返回原地址。如果必须移动,则返回一个新地址,它指向一个更大的内存块。要注意的是如果要将内存块减小,那么会返回原地址。

要注意的是,如果一个内存块是链表或者树的一部分,则必须指定 HEAP_REALLOC_IN_PLACE_ONLY。因为在这种情况下,链表或树的其他节点可能指向当前节点的指针,把节点移动到堆中其他的地方会破坏其完整性。

最后:该函数要么返回一个地址,指向调整后的内存块,要么返回NULL(不能调整)

 

18.3.3 获得内存块的大小

标志位fdwFlags :可以是 0 或者 HEAP_NO_SERIALIZE。

pvMem 是内存块地址。

hHeap 是堆句柄

 

18.3.4 释放内存块

当我们不需要一块内存的时候,可以调用 HeapFree 释放它。

参数意义同上。

注意,调用这个函数可能会使堆管理器撤销一些已经调拨的物理存储器,但是这并不是一定的。

 

18.3.5 销毁堆

如果不需要之前创建的堆,则可以调用 HeapDestroy() 函数来销毁它。

调用该函数会释放堆中所有内存块,同时系统会回收堆占用的物理存储器和地址空间区域。

如果调用成功,返回TRUE。

如果不在进程终止之前调用,那么系统会替我们销毁的。

我们知道线程才是执行体,创建堆是在线程内执行的,但是线程终止的时候,堆是不会被销毁的。

在进程完全终止之前,系统是不会允许销毁进程的默认堆的。所以这时候如果试图销毁,会返回FALSE。

 

18.3.6 在C++中使用堆

运用堆的最好方法就是将它们集成到已有的C++程序中。在C++中,我们调用new操作符来分配对象(而不是malloc函数),然后在不需要某个对象的时候调用delete操作符释放它(而不是free函数)。

具体的不讲了,其实就是把 创建堆啊,分配内存 之类的堆函数 放到new 和 delelte操作符里面去,重载这两个操作符。当然我们需要 static 来保存堆的句柄,然后用一个static 的计数来表明有多少个对象生成了。每销毁一个则减一,当减到0的时候表示堆空了,可以销毁了。

要注意的情况是:继承! 如果父类和子类的对象大小相差很大,而且子类没有重载操作符,则可能会产生严重的碎片问题。这样我们创建堆的意义也就不存在了。

 

18.4 其他堆函数

由于进程在自己的地址空间中可以有多个堆,所以GetProcessHeaps 函数可以让我们得到这些堆的句柄:

用法示例如下:

要注意的是,返回的句柄包括进程的默认堆句柄。

 

下面这个函数可以用来验证堆的完整性:

传入一个堆句柄,标志位传入0,然后给pvMem传递NULL即可。

如果想判断指定内存块是否被破坏,则给pvMem传递指定内存块地址。

 

下面的函数作用是将堆中闲置内存块重新结合在一起,并撤销调拨给堆中闲置内存块的物理存储器。

一般来说会给 fdwFlags 传入参数0

 

下面两个函数用于线程同步:

这两个函数配对使用,当第一个线程调用 HeapLock 时,它就成为指定堆的所有者。假设这时候有一个线程2调用了线程1指定的堆的时候,就会暂停挂起,直到第一个线程调用了HeapUnLock 函数才会被唤醒。

为了确保对堆的访问是依次进行的,前面提到的 HeapAlloc、HeapSize、HeapFree 之类的函数都会在内部调用这两个函数。所以一般情况下是不需要我们调用的。

 

最后有一个 HeapWalk 函数,其作用是遍历堆的内容。

要求传入一个 PROCESS_HEAP_ENTRY 结构的地址

当返回FALSE时表示遍历完。

该结构类型定义如下:

当我们开始枚举的时候,要把 lpData 成员设置为NULL。

每次调用 HeapWalk 成功后,可以查看内部成员。

如果要得到堆中的下一块内存,必须再次调用 HeapWalk ,并传入和上一次调用相同的堆句柄和 上述的数据结构地址。

我们可以在 HeapWalk 循环外部调用 HeapLock 和 HeapUnlock,这样可以保证遍历的时候不会有其他线程从中分配与释放内存了。

大致就是这样…

【Windows核心编程】堆 的应用
Tagged on:
0 0 投票数
Article Rating
订阅评论
提醒

0 评论
内联反馈
查看所有评论