《Windows核心编程系列》十四谈谈默认堆和自定义堆
堆
前面我们说过堆非常适合分配大量的小型数据。使用堆可以让程序员专心解决手头的问题,而不必理会分配粒度和页面边界之类的事情。因此堆是管理链表和数的最佳方式。但是堆进行内存分配和释放时的速度比其他方式都慢,而且无法对物理存储器的调拨和撤销调拨进行控制。
什么是堆?
在系统内部堆就是一块预定的地址空间区域。刚开始堆的大部分页面都没有调拨物理存储器。随着我们不断的从堆中分配内存,堆管理器会给堆调拨越来越多的物理存储器。这些物理存储器始终是从页交换文件中分配的。释放堆中的内存时,堆管理器会撤销已调拨的物理存储器。
进程默认堆。
进程初始化时,系统会在进程的地址空间中创建一个堆。这个堆被称为进程的默认堆。默认情况下,这个堆的地址空间区域大小是1MB。程序员可以控制这个大小。我们可以在创建应用程序时用/HEAP连接器开关来改变默认堆的大小。由于DLL没有与之关联的堆,因此在创建DLL时,不应该使用/HEAP开关。
- /HEAP:reserve[,commit]
由于许多Windows函数会用到进程默认堆,因此对默认堆的访问必须一次进行。系统会保证在任何情况下只让一个线程从默认堆中分配或释放内存块。如果应用程序只有一个线程,而我们又希望以最快的方式访问堆,我们应该创建自己的堆,而不要使用默认堆。
默认堆由系统创建和释放,我们无法销毁默认堆。每个堆都有一个标识自己的句柄,所有分配和释放内存块的堆函数都会在参数中使用到这个堆句柄。我们可以调用GetProcessHeap来得到默认堆的句柄。
- HANDLE GetProcessHeap();
创建额外堆的时机:
一:对数据保护。创建两个或多个独立的堆,每个堆保存不同的结构,对两个堆分别操作,可以使问题局部化。
二:更有效的内存管理。创建额外的堆,管理同样大小的对象。这样在释放一个空间后可以刚好容纳另一个对象。
三:内存访问局部化。将需要同时访问的数据放在相邻的区域,可以减少缺页中断的次数。
四:避免线程同步开销。默认堆的访问是依次进行的。堆函数必须执行额外的代码来保证线程安全性。通过创建额外的堆可以避免同步开销。
五:快速释放。我们可以直接释放整个堆而不需要手动的释放每个内存块。这不但极其方便,而且还可以更快的运行。
创建额外的堆
我们可以调用HeapCreate来创建额外的堆:
- HANDLE HeapCreate(
- DWORD fdwOptions,
- SIZE_T dwInitilialize,
- SIZE_T dwMaximumSize);
fdwOptions表示对堆的操作该如何进行。可以传入0, HEAP_NO_SERIALIZE,
HEAP_GENERATE_EXCEPTIONS,HEAP_CREATE_ENABLE_EXECUTE或这些标志的组合。
HEAP_NO_SERIALIZE告诉堆管理器堆管理器不负责堆的线程安全性。对堆的线程安全性的控制由程序员控制。
HEAP_GENERATE_EXCEPTIONS标志告诉系统,每当在堆中分配或重新分配内存块失败时抛出一个异常。用来通知应用程序有错误发生。
dwInitialSize表示一开始要调拨给堆的字节数。如果需要HeapCreate会把这个值向上取整到cpu页面大小的整数倍。
dwMaximumSize表示堆所能增长到的最大大小。即系统为堆所预定的地址空间的最大大小。如果试图分配的内存块超过最大大小,分配操作会失败。如果dwMaximumSize为0,则表明创建的堆是可增长的,没有一个指定上限。
函数执行成功HeapCreate会返回一个句柄,标识了新创建的堆。
堆创建后,需要从堆中分配内存时,要调用HeapAlloc函数:
- PVOID HeapAlloc(
- HANDLE hHeap,
- DWORD fdwFlags,
- SIZE_T dwBytes);
hHeap是一个堆句柄,表示要从哪个堆分配内存。
fdwFlags用来执行一些标志。这些标志会对分配产生一些影响。总共有三个标志:
HEAP_ZERO_MEMORY,HEAP_GENERATE_EXCEPTIONS和HEAP_NO_SERIALIZE。
HEAP_ZERO_MEMORY会让HeapAlloc返回之前把内存块的内容都清0 。
HEAP_GENERATE_EXCEPTIONS用来告诉系统如果堆中没有足够的内存来满足分配请求,此次调用的
HeapAlloc应抛出异常。可以在创建堆时指定这个标志,只要在这个堆上分配内存,如果内存不足都抛出异常。
如果分配成功HeapAlloc会返回内存块地址。否则将会返回NULL。
默认情况下,对堆的访问会依次进行。当任何程序试图从堆中分配一块内存时,HeapAlloc会执行以下操作:
1:遍历已分配的内存的链表和闲置内存的链表。
2:找到一块足够大的闲置内存块。
3:分配一块新的内存,将2找到的内存块标记为已分配。
4:将新分配的内存块添加到已分配的链表中。
注意:在分配大于1MB的内存时应该避免使用堆函数,而应该使用VirtualAlloc函数。
HeapReAlloc可以改变堆中某一块内存的大小:
- PVOID HeapReAlloc(
- HANDLE hHeap,
- DWORD fdwFlags,
- PVOID pvMem,
- SIZE_T dwBytes);
hHeap用来标识一个堆。
fdwFlags用来在调整内存块大小时用到这些标志。可以有以下标志:HEAP_GENERATE_EXCEPTIONS,HEAP_NO_SERIALIZE,HEAP_ZERO_MEMORY,HEAP_REALLOC_IN_PLACE_ONLY。
前两个标志与前面介绍的一样。只有当增大内存块时HEAP_ZERO_MEMORY才有用。额外的字节会被清0。
在增大内存块时HeapReAlloc可能会移动内存块,HEAP_REALLOC_IN_PLACE_ONLY标志告诉HeapReAlloc尽量不要移动内存块。如果不移动不能增大内存块,则HeapReAlloc返回新地址。
pvMem指定要调整大小的内存块。
dwBytes指定内存块的新大小。
分配一块内存后,调用HeapSize可以获得这块内存的实际大小:
- SIZE_T HeapSize(
- HANDLE hHeap,
- DWORD fdwFlags,
- LPCVOID pvMem);
hHeap用来标识堆。
pvMem表示内存地址。
dwFlags可以是0或HEAP_NO_SERIALIZE
当不要使用一块内存时可以调用HeapFree来释放它:
- BOOL HeapFree(
- HANDLE hHeap,
- DWORD fdwFlags,
- PVOID pvMem);
如果操作成功则返回TRUE。调用这个函数可能会使堆管理器撤销一些已经调拨的物理存储器。
如果应用程序不再需要自己创建的堆,可以调用HeapDestroy来销毁它:
- BOOL HeapDestroy(HANDLE hHeap);
此时系统会收回堆所占用的物理存储器和地址空间区域。执行成功则返回TRUE。如果我们不调用此函数主动销毁自己创建的堆,在进程结束时,系统会替我们销毁。我们不能调用此函数销毁默认堆,默认堆由系统管理。
在C++中使用堆
在C++中我们可以调用new操作符来分配类对象。不需要时可以调用delete来释放它。如
- CA *pCA=new CA;
在编译此段代码时,编译器会首先检查类CA是否重载了new操作符成员函数。如果找到编译器会调用这个函数。否额,会调用C++标准的new操作符。
- deleted pCA;
对此句代码C++编译器会执行与上面类似的步骤,只有CA类没有重载delete操作符成员函数时,才会调用标准的C++delete运算符。
通过对C++类的new和delete操作符进行重载,我们可以非常容易的将堆函数加以运用:
- class CA
- {
- public:
- CA();
- ~CA();
- public:
- void *operator new(size_t size);
- void*operator delete(void*p);
- };
上述代码调用operator new和operator delete是从默认堆中分配的内存。我们可以让其在自己创建的堆中分配内存,一般让所有对象共享同一个堆,每个对象都创建一个堆为导致额外的性能开销。可以采用计数法来对堆的生存期进行控制。
ToolHelp函数允许我们枚举进程的堆以及分配的内存块。它包括一下函数:Heap32First,Heap32Next,Heap32ListFirst和Heap32ListNext。
由于进程在自己的地址空间可以有多个堆,GetProcessHeaps可以让我们得到这些堆的句柄。
- DWORD GetProcessHeaps(
- DWROD dwNumHeaps,
- PHANDLE phHeaps);
phHeaps是一个数组指针。用以存储返回的堆句柄。
dwNumHeaps是数组大小。
函数返回句柄数组个数。
函数所返回的句柄数组中也包括进程的默认堆的句柄。
- HANDLE hHeaps[20];
- DWORD dwHeaps=GetProcessHeaps(20,hHeaps);
HeapValidate可以验证堆的完整性。
- BOOL HeapValidate(
- HANDLE hHeap,
- DWORD fdwFlags,
- LPCVOID pvMem);
通常在调用这个函数时,我们会传一个堆句柄和一个标志0,并传入NULL给pvMem。该函数会遍历堆中的各个内存块,确保没有任何一块内存被破坏。如果给pvMem制定一块内存地址,那么函数就只检查这一块内存的完整性。
为了让堆中闲置的内存块能重新结合在一起,并撤销调拨给堆中闲置内存块的存储器,可以调用HeapCompact:
- UINT HeapCompact(
- HANDLE hHeap,
- DWORD fdwFlags);
一般来说会传0给fdwFlags。
下面两个函数要配对使用,用于线程同步:
- BOOL HeapLock(HANDLE hHeap);
- BOOL HeapUnlock(HANDLE hHeap);
当第一个线程调用HeapLock时,它就占有了堆。其他线程在调用堆函数时,系统就会暂停其他线程。只有当第一个线程调用HeapUnlock之后才会唤醒被暂停的进程。
HeapAlloc,HeapSize,HeapFree这些函数会在内部调用HeapLock和HeapUnlock,一般来说不需要自己去调用HeapLock和HeapUnlock。
最后一个函数是HeapWalk,它允许我们遍历堆的内容。只用于调试。具体不再介绍。
以上参考自《Windows核心编程》第五版第三部分,如有纰漏,请不吝指正!!