PPC的C/C++和人工智能学习笔记
每一篇学习笔记,都只是为了更好地掌握和理解

Win32SDK(13)_堆内存

今天学习:Windows堆内存

 

堆内存适合分配大量的小型数据。堆是用来管理链表和树的最佳方式。堆的优点是它能让我们专心解决手头的问题,而不必关心分配粒度和页面边界这些问题;堆的缺点是分配和释放内存块的速度比其他方式慢,也无法再对物流存储器的调拨和撤销进行直接控制。

系统内部,堆就是一块预订的地址空间区域。开始的时候,区域内大部分页面没有调拨物流存储器,我们在不断从堆中分配内存的时候,堆管理器会给堆调拨越来越多的物流存储器。这些物流存储器始终是从页交换文件中分配的。我们在释放堆中的内存时,堆管理器会撤销已经调拨的物理存储器。

 

进程的默认堆:

进程初始化时,系统会在进程的地址空间中创建一个堆(称为进程的默认堆,default heap)。默认情况下是1M,但是在使用过程中系统可以增大它。也可以在创建应用程序时用/HEAP:reserve[,commit]开关来改变默认的区域大小(注意:动态链接库DLL没有与之关联的堆)。

系统保证在任何时候,一次只能让一个线程从默认堆中分配或释放内存块。一个进程同时可以有多个堆,但是默认堆是在进程开始运行前由系统自动创建的,在进程终止后会自动销毁。我们无法销毁默认堆。

通过GetProcessHeap函数来获得进程默认堆的句柄。HANDLE WINAPI GetProcessHeap(VOID);

 

为什么要创建额外的堆:

  • 对组件进行保护:假设程序需要处理2个组件,一个是链表、一个是二叉树,如他们同时保存在一个堆中,那么链表和二叉树的节点之间会互相夹杂,假如链表的代码有BUG,数据越界操作而损坏了二叉树的数据,会让我们以为是二叉树的代码有问题,从而引起混淆,也对BUG的跟踪和定位造成困难。而创建2个堆来分别保存这2个组件,则容易让问题局部化。
  • 更有效的内存管理(减少内存碎片):我们希望每个堆存放的对象有相同的大小,这样在释放、分配内存块的时候可以匹配,减少内存碎片。
  • 使内存访问局部化:当系统必须把一个内存页面换出到页交换文件,或者把页交换文件中的一个页面换入到内存,会对性能产生非常大的影响。如果把内存访问局限在一个较小的地址区间内,将降低系统需要在内存和磁盘之间进行交换的可能性。比如前面说的2个组件的夹杂存储,我们在遍历链表或者二叉树的时候,由于地址区间较大,内存和磁盘之间交换数据的可能性就会加大。而用2个堆分别存放2个组件,则会降低该可能性。
  • 避免线程同步的开销:我们知道,默认堆的访问是依次进行的,也就是说即使多个线程同时访问堆,也不会出现数据被破坏的情况。但是,堆函数必须执行额外的代码来保证堆的线程安全性(Thread-Safe),如果在堆中进行大量的分配操作,那么执行这些额外的保证线程安全的代码的性能开销就不能被忽略。如果选择新建一个堆,可以告诉系统只有一个线程会访问该堆,这样就不需要执行额外的保证线程安全的代码了。当然,此时我们应该明白:保证堆的线程安全性,是我们自己的任务了,系统不再帮我们管这些事情了。
  • 快速释放:把一些数据结构存入一个专门的堆中,我们可以直接释放整个堆而不必显式地释放堆中的每个数据块,方便而且快速。

 

如何创建额外的堆:

创建堆HeapCreate

HANDLE WINAPI HeapCreate( //返回一个堆句柄

_In_ DWORD flOptions,

_In_ SIZE_T dwInitialSize,

_In_ SIZE_T dwMaximumSize);

参数flOptions,如何操作堆,可以是0、HEAP_NO_SERIALIZE、HEAP_GENERATE_EXCEPTIONS、HEAP_CREATE_ENABLE_EXECUTE等标志或标志的组合。

HEAP_NO_SERIALIZE标志:默认情况下(没有HEAP_NO_SERIALIZE标志),堆的访问是线程安全的,一旦加上HEAP_NO_SERIALIZE标志,堆的线程安全性系统将不再维护,所以一般满足以下条件,我们才能使用该标志:

  • 进程中只有一个线程;
  • 进程中有多个线程,但是只有一个线程会访问该堆;
  • 进程中有多个线程,但进程使用了其他方式来管理对堆的独占访问,比如临界区、互斥量、信号量等等。
  • 假如自己也不清楚到底是不是要使用HEAP_NO_SERIALIZE标志,那么就不要用,虽然这样做在调用堆函数的时候对性能产生轻微的影响,但是避免了堆中数据被破坏的危险。

HEAP_GENERATE_EXCEPTIONS标志:加上该标志,则当堆中分配或出现分配内存块失败时抛出一个异常。异常只不过是系统所使用的另一种方法,用来通知应用程序有错误发生。在设计应用程序的时候,有时候捕获异常比检查返回值要容易。

HeapSetInformation(NULL,HeapEnableTerminationOnCorruption,NULL,0);

使用该代码设置在任何Heap*函数中一旦发现堆破坏,就抛出异常。注意:一旦为进程中的所有堆启用了该特性,就再也无法禁用它了。

HEAP_CREATE_ENABLE_EXECUTE标志:如果想在堆中存放可执行代码,就加上该标志。没有该标志时,尝试在堆的内存块中执行代码,系统抛出EXCEPTION_ACCESS_VIOLATION异常。

HeapCreate的第2个参数dwInitialSize表示一开始要调拨给堆的字节数。第3个参数dwMaximumSize表示堆所能增长到的最大大小,假如为0,则堆的大小没有指定上限,

 

堆中分配内存块HeapAlloc

LPVOID WINAPI HeapAlloc(//返回内存块的地址,失败返回NULL

_In_ HANDLE hHeap, //堆句柄

_In_ DWORD dwFlags,//标志

_In_ SIZE_T dwBytes); //分配的字节数

参数dwFlags标志:

HEAP_ZERO_MEMORY:内存块内容清零。

HEAP_GENERATE_EXCEPTIONS:堆中没有足够的内存满足分配请求,就抛出异常。假如在HeapCreate时已经指定了该标志,这里可以不再指定。当然,也可以在HeapCreate中不使用该标志,而此时该标志就只会影响到此次的调用。

HEAP_NO_SERIALIZE:同HeapCreate的标志类似,表示此次操作不考虑线程安全性。在默认堆操作时,绝对不要加上该标志!

注意:在分配大块内存(>=1M)时,应避免使用堆,建议使用VirtualAlloc(虚拟内存)。

 

HeapAlloc的操作次序:

  • 遍历已分配内存的链表和闲置内存的链表;
  • 找到一块足够大的闲置内存块;
  • 分配一块新的内存,也就是将刚找到的闲置内存块标记为已分配;
  • 将新分配的内存块添加到已分配内存的链表中。

 

低碎片堆(lowfragmentation heap)算法:

如果需要分配大量不同大小的内存块,堆管理器内部用来处理分配请求的默认算法可能会产生地址空间碎片,可以使用强制的方法,使系统切换到低碎片堆的算法,在多处理器的机器上,低碎片堆的性能得到了极大的提高。

ULONG HeapInfomationValue = 2;

if (HeapSetInformation(hHeap, HeapCompatibilityInformation,

&HeapInfomationValue, sizeof(HeapInfomationValue))) {

cout << “转换为低碎片堆成功。” << endl;

}

else {

cout << “转换为低碎片堆失败。” << endl;

cout << “HEAP_NO_SERIALIZE标志创建的堆无法转换为低碎片堆。” << endl;

}

 

调整内存块的大小HeapReAlloc:

LPVOID WINAPI HeapReAlloc(//返回地址,错误返回NULL

_Inout_ HANDLE hHeap, //堆句柄

_In_ DWORD dwFlags, //标志

_Frees_ptr_opt_ LPVOID lpMem, //当前地址

_In_ SIZE_T dwBytes); //调整后的大小字节数

参数dwFlags标志:类似HeapAlloc。HEAP_GENERATE_EXCEPTIONS、HEAP_NO_SERIALIZE

HEAP_ZERO_MEMORY:该标志只有在增大内存块时,额外增大的部分会被清零。

在增大内存块时,HeapReAlloc可能会在堆内部移动内存块,而HEAP_REALLOC_IN_PLACE_ONLY标志告诉HeapReAlloc不要移动内存块。假如不需要移动内存块就可以增大,它将返回内存块原地址。否则函数会返回新的内存块地址。所以,我们在操作链表、树等数据结构时,应该避免让内存块移动,这会导致其他节点的指向出错。

 

获得内存块的大小HeapSize:

SIZE_T WINAPI HeapSize(//获得分配内存块的实际大小

_In_ HANDLE hHeap, //堆句柄

_In_ DWORD dwFlags, //0 或者 HEAP_NO_SERIALIZE

_In_ LPCVOID lpMem); //内存块的地址

 

释放内存块HeapFree:

BOOL WINAPI HeapFree(//成功返回TRUE,失败FALSE

_Inout_ HANDLE hHeap, //堆句柄

_In_ DWORD dwFlags,//0 或者HEAP_NO_SERIALIZE

__drv_freesMem(Mem) _Frees_ptr_opt_ LPVOID lpMem);

调用该函数有可能会使堆管理器撤销一些已经调拨的物理内存,但不一定。

 

销毁堆HeapDestroy:

BOOL WINAPI HeapDestroy( _In_ HANDLE hHeap );

调用HeapDestroy会释放堆中所有的内存块,同时系统会收回堆所占用的物流存储器和地址空间区域。进程终止时,没有显式销毁的堆也会销毁。但是假如一个线程创建了堆,线程终止时,该堆是不会销毁的。

默认堆不允许销毁,强行HeapDestroy默认堆会返回FALSE。

 

其他堆函数:

ToolHelp函数可以枚举进程的堆以及堆中分配的内存块。Heap32First、Heap32Next、Heap32ListFirst、Heap32ListNext。<tlhelp32.h>

 

获取进程中所有的堆句柄GetProcessHeaps:

DWORD WINAPI GetProcessHeaps(//返回获取的堆句柄数量

_In_ DWORD NumberOfHeaps, //数组的大小

_Out_writes_to_(NumberOfHeaps, return) PHANDLE ProcessHeaps);//堆句柄数组

如:

HANDLE heaps[20];

DWORD num = GetProcessHeaps(20, heaps);

 

堆完整性验证HeapValidate:

BOOL WINAPI HeapValidate(

_In_ HANDLE hHeap, //堆句柄

_In_ DWORD dwFlags,//0 或者HEAP_NO_SERIALIZE

_In_opt_ LPCVOID lpMem);//NULL表示全检查,指定内存块的地址的话,就只检查这一块内存的完整性

 

HeapLock、HeapUnLock堆锁和解锁。

HeapWalk遍历堆的内容。

 

使用自己堆的一个实例:

类XClass将重载new和delete操作符,使该类在自己的堆中分配和释放内存。

//使用自己的堆存放类
#include <Windows.h>
#include <iostream>
#include <tchar.h>
using std::cout;
using std::endl;

//类XClass
class XClass {
public:
 void* operator new(size_t size); //new重载
 void operator delete(void* p); //delete重载
private: 
 static HANDLE s_hHeap; //静态,堆句柄
 static UINT s_uNumAllocsInHeap; //实例数量
 int m_a; 
 char m_str[10];
};

HANDLE XClass::s_hHeap = NULL;
UINT XClass::s_uNumAllocsInHeap = 0;

void* XClass::operator new(size_t size) {
 if (NULL == s_hHeap) { //堆还没有创建
 s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0); //初始0,自动增长
 //注意,上面的HEAP_NO_SERIALIZE小心使用,只有确保单线程使用的情况下才可以用
 if (NULL == s_hHeap) return NULL; //创建堆失败
 void *p = HeapAlloc(s_hHeap, 0, size); //类实例分配内存
 if (NULL != p) 
 ++s_uNumAllocsInHeap; //实例数量+1
 return p;
 }
}

void XClass::operator delete(void* p) {
 if (HeapFree(s_hHeap, 0, p)) 
 --s_uNumAllocsInHeap; //释放实例内存,数量-1
 if (s_uNumAllocsInHeap == 0) {
 if (HeapDestroy(s_hHeap)) //没有实例时,销毁堆
 s_hHeap = NULL;
 }
}

int main() {
 XClass *pX = new XClass;
 //代码使用。。。
 delete pX;
 return 0;
}

 

 

 

(2017-06-17 www.vsppc.com)

学习笔记未经允许不得转载:PPC的C/C++和人工智能学习笔记 » Win32SDK(13)_堆内存

分享到:更多 ()

评论 抢沙发

评论前必须登录!