《Windows核心编程》读书笔记十八章 堆

第十八章 堆

本章内容

18.1 进程的默认堆

18.2 为什么要创建额外的堆

18.3 如何创建额外的堆

18.4 其他堆函数


与虚拟内存和内存映射文件相比,堆非常适合大量的小型数据。堆是用来管理链表和树的最佳方式。

堆能让我们专心心解决问题,不必理会分配粒度和页面边界的这类很底层的事情。堆的缺点是分配和释放速度比其他方式慢,而且也无法对物理存储器的调拨和撤销进行直接控制。


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


MS并未公开堆的调拨和撤销调拨等规则。如果对性能要求极高需要自己直接操作,应该使用VirtualAllocVirtualFree


18.1 进程的默认堆

进程初始化的时候,系统会在其地址空间创建一个默认堆。这个堆的地址空间区域大小是1MB。但是系统可以增大进程的默认堆,使它大于1MB。也可以使用/HEAP连接器开关来控制默认堆的大小

由于DLL没有与之关联的堆,在创建DLL时不应该使用/HEAP开关

/HEAP:reserve[,commit]

另外windows16下的LocalAlloc和GlobalAlloc二回从进程默认堆中分配内存。


windows保证默认堆的访问是线程安全的(一个线程在访问,另一个试图访问的线程会被挂起等待)。这可能会对性能产生轻微的影响。

如果应用程序只有一个线程,而又希望以最快的速度访问堆,可以创建自己的堆而不使用进程的默认堆。遗憾的是无法控制windows函数来使用自己创建的堆(只能使用默认堆)

默认堆在进程创建的时候创建的,在进程终止后自动销毁。

WINBASEAPI
HANDLE
WINAPI
GetProcessHeap(
    VOID
    );


18.2 为什么要创建额外堆

1)对组件进行保护

2)更有效的内存管理

3)局部访问

4)避免线程同步开销

5)快速释放


18.2.1 对组件进行保护

例如应用程序使用一个链表和二叉树的数据结构,如果使用一个堆。那么堆中就会混着链表和二叉树的数据。如果由于其中一个数据结构存在代码缺陷会可能会影响到另一个数据结构。这样问题不方便进行排查。

通过创建两个独立堆,就可以使问题局部化。链表中的缺陷不会破坏二叉树的完整性,反之亦然。



18.2.3 更有效的内存管理

如果始终从堆中分配同样大小的对象,我们就可以对它进行更有效的管理。例如每个NODE结构24字节,每个BRANCH结构需要32字节。所有对象从一个堆中分配。如果释放了NODE2和NODE4堆中将出现碎片。这时如果要创建一个新的BRANCH结构即使堆存在48字节的可用空间,分配操作仍然会失败。

如果每个堆只包含同样大小的对象,那么释放一个对象可以保证释放出的空间刚好能够容纳另一个对象。





18.2.3 使内存访问局部化。

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

如果把内存访问局限在一个较小的地址区间内,将降低系统需要在内存和磁盘之间进行交换的可能性。


因此在设计的时候,一种很好的做法是把需要同时访问的对象分配在相邻的内存地址。

一种很好的做法是把需要同时访问的对象分配在相邻的内存地址。

如果一个对象的数据分散最差的情况下,每个内存页面中只有一个NODE对象,这种情况下,遍历链表的每个NODE都会引起错误,从而导致整个遍历过程极其缓慢。


18.2.4 避免线程同步的开销

由于堆是线程安全的,堆函数会执行额外代码来保护堆。如果对堆执行大量额外操作,这些额外操作带来的性能损失就会累积。
可以创建自己的堆,但是线程安全需要我们自己来维护了。

18.2.5 快速释放

例如显示目录树状结构树的时候会构建一颗树,但是刷新以后需要重新构建。如果一个一个的删除树节点这效率极低。
可以直接创建一个堆来保存这棵树,然后直接销毁这个堆。并重新来一遍。

18.3 如何创建额外的堆


调用 HeapCreate来创建自己的堆
WINBASEAPI
_Ret_maybenull_
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组合

默认堆具有线程安全。任何程序试图从堆中分配内存的时候。 HeapAlloc会执行以下操作:
1)遍历已分配内存的链表和闲置内存链表
2)找到一块足够大的闲置内存块
3)分配一块新的内存,也就是将刚找到的闲置内存块标记为已分配。
4)将新分配的内存块添加到已分配内存的链表中。

一个例子解释了不应该使用 HEAP_NO_SERIALIZE标志。
假设有两个线程试图在同一个时刻从同一个堆中分配内存。
#1线程执行步骤1)和2)得到了闲置内存块的地址,但是当#1线程准备执行3的时候,系统让线程1暂停,这样#2线程也有机会执行步骤1)和2)。
由于#1线程还没执行步骤3) 因此#2线程找到的闲置内存块与#1线程相同。

最后两个线程都以为自己找到了一块闲置内存,#1线程对链表更新,将新的内存块标记为已分配。#2线程也对链表进行更新,将同一块内存标记为已分配。
两个线程都拿到了同一块内存。但谁也没发现问题。

这种问题不会立即暴露后续是非常难以排查的。可能导致以下问题:
1)内存块的链表被破坏。这个问题只有下次分配或释放内存的时候才会被发现。
2)两个线程共享一块内存。同时写入信息会破坏各自运行的逻辑。
3)其中一个线程可能会释放它,这使得另一个线程覆盖已经释放的内存,从而导致堆被破坏。

这些问题的解决方案是只允许一个线程独占堆以及其链表的访问。也就是不指定 HEAP_NO_SERIALIZE
满足一下条件的时候才可以使用 HEAP_NO_SERIALIZE
1)进程只有一个线程
2)进程有多个线程,但只有一个线程会访问这个堆
3)进程有多个线程,但是进程使用了其他线程同步机制来管理堆的独占访问。(如critical section, mutex, semaphore ,srwlock)

HEAP_GENERATE_EXCEPTIONS 
告知系统每当在堆中分配或重新分配内存块失败的时候,抛出一个异常。



如果要在堆中存放可执行代码。必须使用 HEAP_CREATE_ENABLE_EXECUTE
如果不设置这个标志,试图执行堆内存块中的代码会抛出 EXCEPTION_ACCESS_VIOLATION异常

HeapCreate的第二个参数 dwInitialSize表示一开始要调拨给堆的字节数。会向上取整到CPU页面大小的整数倍。
dwMaximumSize表示堆所能增长到的最大大小。如果其值大于0 系统会给所创建的堆设置一个上限,如果分配内存超过上限,分配操作会失败。
如果 dwMaximumSize为0.那么所创建的堆是可增长的,它没有一个指定的上限。直到用尽所有物理内存为止。


18.3.1 从堆中分配内存块

调用一下函数
WINBASEAPI
_Ret_maybenull_
_Post_writable_byte_size_(dwBytes)
LPVOID
WINAPI
HeapAlloc(
    _In_ HANDLE hHeap,
    _In_ DWORD dwFlags,
    _In_ SIZE_T dwBytes
    );

Heap是堆的句柄。
dwBytes表示从堆中分配多少字节。
dfwFlags指定一些标志。  
HEAP_ZERO_MEMORY,  所分配的内存块内容清零
HEAP_GENERATE_EXCEPTIONS 如果堆没有足够内存来满足内存分配就抛出异常(如果在创建堆的时候已经指定此标志则这里可以不再设定此标志)

如果HeapAlloc抛出异常可能是以下值:


如果没有指定 HEAP_GENERATE_EXCEPTION那么无法分配内存会返回NULL值。


HEAP_NO_SERIALIZE 
强制系统不要把这次HeapAlloc调用与其他线程对同一个堆的访问一次排列起来(非线程安全)



如果分配大量不同大小的内存块,那么堆管理器内部用来处理分配请求的默认算法可能会产生地址空间碎片。
在xp和2003以后的版本可以强制操作系统在分配内存的时候使用一种叫低碎片堆(lowfragmentation heap)的算法。能极大提高性能。
以下代码用来切换到低碎片堆:
	ULONG HeapInformationValue = 2;
	if (HeapSetInformation(
		hHeap, HeapCompatibilityInformation,
		&HeapInformationValue, sizeof(HeapInformationValue))){
		// hHeap is turned into a low fragmentation heap
	}
	else {
		// hHeap can't be turned into a low fragementation heap.
		// Maybe because it has been created with the HEAP_NO_SERIALIZE flag
	}

例如把GetProcessHeap返回值传给HeapSetInformation ,默认堆就好变成一个低碎片堆。 如果传入堆句柄是用HEAP_NO_SERIALIZE创建的,那么HeapSetInformation会调用失败。如果代码是在调试模式下运行,有些代码会诅咒堆变成一个低碎片堆。把环境变量_NO_DEBUG_HEAP设为1,就可以关闭这些堆调试选项。
堆管理器自己也会堆所有分配请求进行监控并进行一些内部优化,如果堆管理器发现切换到低碎片堆会对应用程序有好处,那么它可能会自动进行切换。

18.3.2 调整内存块的大小


有些应用需要在运行时调整已分配内存块的大小。使用HeapReAlloc
WINBASEAPI
_Success_(return!=0)
_Ret_maybenull_
_Post_writable_byte_size_(dwBytes)
LPVOID
WINAPI
HeapReAlloc(
    _Inout_ HANDLE hHeap,
    _In_ DWORD dwFlags,
    _Frees_ptr_opt_ LPVOID lpMem,
    _In_ SIZE_T dwBytes
    );

hHeap表示调整大小的内存块属于哪个堆。
fdwFlags指定一些标志
HEAP_GENERATE_EXCEPTIONS 同heapalloc
HEAP_NO_SERIALIZE 同heapalloc
HEAP_ZERO_MEMORY 只有增大内存块才有用,内存中额外的直接会被清零,减小内存块大小时,这个标志不起任何作用。
HEAP_REALLOC_IN_PLACE_ONLY 告知函数能不移动内存块。
如果内存块是链表或树的一个节点,则需要指定HEAP_REALLOC_IN_PLACE_ONLY.这种情况下,链表或树的其他节点可能包含指向当前节点的指针,把节点移动到堆的其他地方会破坏链表或树的完整性。

pvMem 内存块的地址
dwBytes 想要调整的大小

18.3.3 获得内存块的大小

获得一个内存块的大小。
WINBASEAPI
SIZE_T
WINAPI
HeapSize(
    _In_ HANDLE hHeap,
    _In_ DWORD dwFlags,
    _In_ LPCVOID lpMem
    );
hHeap标识堆
dwFlags 可以是0 或者 HEAP_NO_SERIALIZE
lpMem内存地址。

18.3.4 释放内存块

WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
HeapFree(
    _Inout_ HANDLE hHeap,
    _In_ DWORD dwFlags,
    __drv_freesMem(Mem) _Frees_ptr_opt_ LPVOID lpMem
    );
dwFlags是0 或 HEAP_NO_SERIALZE
这个函数可能会使堆管理器撤销一写已经调拨的物理存储器,但这不是一定的。


笔者发现使用malloc分配的堆内存块并不能直接用HeapSize或者HeapFree来操作,直接导致内存错误。

可能malloc在上层做一些操作返回的不一定是堆的首地址。


18.3.5 销毁堆

WINBASEAPI
BOOL
WINAPI
HeapDestroy(
    _In_ HANDLE hHeap
    );

该函数会释放堆中保护的所有内存块,同时系统会回收堆所占用的物理存储器和地址空间区域。
如果函数调用成功将返回TRUE。
如果创建了自己的堆在进程终止以前系统并不会销毁,所以要自行销毁。
如果把系统默认堆的句柄传给HeapDestroy会直接返回FALSE

18.3.6 在C++中使用堆

C++中通常调用new操作符来分配对象,而不是调用c运行库的malloc函数。
在不需要某个对象,调用delete来释放,而不是调用c运行库的free

例如一下代码:

CSomeClass * pSomeClass = new CSomeClass;
系统会检查是否重载了new操作符,如果没有就用c++默认的new操作符。
否则会用重载的new操作符来创建这个对象。

在用完以后调用delete来销毁对象。
delete pSomeClass;

通过重载new 和delete就可以将堆函数加以运用。


定义了一个在堆上分配的类。
头文件中
class CSomeClass {
private:
	static HANDLE s_hHeap;	// store the handle of the heap
	static UINT s_uNumAllocsInHeap; // counter to record the number of instances of CSomeClass

	// Other private data and member functions
	// ...

public:
	void * operator new (size_t size);
	void operator delete (void * p);
	// other public data and member function
	// ...
};

cpp文件
HANDLE CSomeClass::s_hHeap = NULL;
UINT CSomeClass::s_uNumAllocsInHeap = 0;

void * CSomeClass::operator new (size_t size){
	if (s_hHeap = NULL) {
		// Heap does not exist; create it.
		s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);

		if (s_hHeap == NULL)
			return NULL;
	}

	// The heap exists for CSomeClass objects.
	void *p = HeapAlloc(s_hHeap, 0, size);

	if (p != NULL) {
		// Memory was allocated successfully; increment
		// the count of CSomeClass objects in the heap.
		s_uNumAllocsInHeap++;
	}

	// Return the address of the allocated CSomeClass object.
	return p;
}

重载delete操作符

void CSomeClass::operator delete (void *p) {
	if (HeapFree(s_hHeap, 0, p)) {
		// Object was deleted successfully.
		s_uNumAllocsInHeap--;
	}

	if (s_uNumAllocsInHeap == 0) {
		// If there are no more objects in the heap,
		// destroy the heap.
		if (HeapDestroy(s_hHeap)) {
			// Set the heap handle to NULL so that the new operator
			// will know to create a new heap if a new CSomeClass
			// object is created.
			s_hHeap = NULL;
		}
	}
}


如果以CSomeClass为基类派生一个新类,那么新类将继承基类的new和delete操作符。并且会使用和基类同一个堆。
如果想在派生类中使用一个单独的堆,把CSomeClass类中的代码复制一份即可。也就是增加一组s_hHeap和s_uNumAllocsInHeap变量,把new和delete操作符的代码复制过去即可。

18.4 其他堆函数

windows还提供了一些堆函数。
ToolHelp 允许枚举进程堆以及堆中分配的内存块。
Heap32First, Heap32Next, Heap32ListFirstHeap32ListNExt.

获得进程所有堆的句柄

再不使用堆分配任何内存的情况下,只有一个堆。



笔者混用了malloc和HeapAlloc来分配内存,结构最后获取的dwHeaps居然有6个堆。





HeapValidate来验证堆的完整性

WINBASEAPI
BOOL
WINAPI
HeapValidate(
    _In_ HANDLE hHeap,
    _In_ DWORD dwFlags,
    _In_opt_ LPCVOID lpMem
    );

dwFlags是 HEAP_NO_SERIALIZE 或0
lpMem传入一个内存地址。或者0  
函数会遍历堆中所有内存块确保其完整性。


WINBASEAPI
SIZE_T
WINAPI
HeapCompact(
    _In_ HANDLE hHeap,
    _In_ DWORD dwFlags
    );
让堆中闲置的内存块能连续在一起并撤销调拨给对重闲置内存块的物理存储器。
一般dwFlags传递0. 也可以传 HEAP_NO_SERIALIZE

WINBASEAPI
BOOL
WINAPI
HeapLock(
    _In_ HANDLE hHeap
    );


WINBASEAPI
BOOL
WINAPI
HeapUnlock(
    _In_ HANDLE hHeap
    );

用于线程同步。当某个线程要访问堆的链表结构时(例如申请分配内存)其他任何线程试图访问堆都会被挂起。
只有当线程调用了HeapUnlock以后才会唤醒被挂起的线程。

为了确保线程安全,HeapAlloc, HeapSize, HeapFree的内部都会调用HeapLock和HeapUnlock 一般情况不需要执行调用。

WINBASEAPI
BOOL
WINAPI
HeapWalk(
    _In_ HANDLE hHeap,
    _Inout_ LPPROCESS_HEAP_ENTRY lpEntry
    );

用于调试,运行遍历堆的内容
需要创建一个PROCESS_HEAP_ENTRY的结构并传地址给以上函数

typedef struct _PROCESS_HEAP_ENTRY {
    PVOID lpData;
    DWORD cbData;
    BYTE cbOverhead;
    BYTE iRegionIndex;
    WORD wFlags;
    union {
        struct {
            HANDLE hMem;
            DWORD dwReserved[ 3 ];
        } Block;
        struct {
            DWORD dwCommittedSize;
            DWORD dwUnCommittedSize;
            LPVOID lpFirstBlock;
            LPVOID lpLastBlock;
        } Region;
    } DUMMYUNIONNAME;
} PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY, *PPROCESS_HEAP_ENTRY;

开始枚举内存块的时候必须把lpData成员设置为NULL。每次调用HeapWalk成功以后,可以查看结构成员。要得到下一块内存,必须再次调用HeapWalk并传入上次调用时相同的句柄和PROCESS_HEAP_ENTRY结构地址。 当HeapWalk返回FALSE时,表明堆中已经没有更多内存块了。

可以在HeapWalk循环外部调用HeapLock和HeapUnlock来锁定堆。在遍历的时候其他线程无法分配和释放内存。

一个遍历当前进程所有Heap和内存块的例子

void printHeapBlock(HANDLE hHeap) {
	if (hHeap == NULL)
		return;

	PROCESS_HEAP_ENTRY pEntry = { 0 };
	pEntry.wFlags = PROCESS_HEAP_REGION;	// query the first virtual address used by the region.
	unsigned int nCount = 0;
	printf("Print the blocks of heaps: %p\n", hHeap);
	HeapLock(hHeap);	// lock the heap.
	while (HeapWalk(hHeap, &pEntry)) {
		printf("Block[%d]\t%p\tsize:%d bytes\n",
			nCount, pEntry.lpData, pEntry.cbData);
		nCount++;
	}
	HeapUnlock(hHeap);
	printf("Total %d blocks.\n", nCount);
}

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	
	HANDLE hHeaps[25];	// assume the number of heaps is not greater than 25.
	DWORD dwHeaps = GetProcessHeaps(25, hHeaps);
	for (DWORD id = 0; id < dwHeaps; id++) {
		printHeapBlock(hHeaps[id]);
	}

	return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值