• 第18章 堆


    18.1 进程的默认堆

    (1)堆的特点

      ①不必考虑分配粒度和页面边界问题,但分配和释放内存的速度比其他方式慢

      ②堆是系统从页交换文件中预订的一块地址空间,系统会负责调拨和撤销调拨物理存储器。

    (2)进程默认堆

      ①进程初始化时,系统会在进程地址空间中一个特殊的区域,这个区域为进程的默认堆(默认为1MB),也可以使用/HEAP链接器开关改变默认区域的大小,使用方法为/HEAP:reserve,[commit]

      ②许多Windows函数要用到默认堆,如ANSI版的Windows函数在底层必须先把ANSI字符串转为Unicode,再调用其Unicode版本(因为内核是Unicode版的)

      ③默认堆是进程私有的,不能为其他进程所共享。GlobalAllocLocalAlloc函数也是在默认堆是分配内存的。在使用剪贴板时要用GlobalAlloc从默认堆分配内存,在调用SetClipboardData时,系统会将该内存映射到进程高地址(内核分区),以便让其他进程可共享,这也就是调用GlobalAlloc时要指定为GMEM_MOVABLE的原因。

      ④对进程的默认堆的访问是串行化的。两个线程不能同时访问,只能等另一个线程结束对默认堆的访问之后才能访问(这可以防止堆被错误的分配和释放)。

      ⑤进程可以有多个堆,但默认堆只有一个。且默认堆的生命周期与进程一样,进程开始时由系统自动创建,进程结束时自动销毁。

    (3)获取默认堆句柄的方法:HANDLE GetProcessHeap();

    18.2 为什么要创建额外的堆

     

    (1)对组件进行保护:(如图1所示)

      ①假设链表代表一个缺陷,不小心覆盖了NODE1后面的8个字节,从而破坏了BRANCH3结构体中的数据。BinTree.cpp中的代码后来在遍历二叉树时,可能因这个原因而失败。

      ②以上的错误,会造成一种假象,好象是BinTree.cpp代码出现错误,而实际上是ListList.cpp的代码有缺陷,这种错误很难跟踪与定位。

      ③可以创建两个独立的堆,一个用来保存Node结构,另一个用来保存Branch结构,这样可以使问题局部化。

    (2)更有效的内存管理(如图2所示)

      ①假NODE结构为24字节,BRANCH为32字节,此时正好占满整个堆。如果释放NODE2和NODE4虽然可以回收48字节的空间,但会出现内存碎片,现在我们需要分配一个BRANCH结构时会出现失败。

      ②如果创建两个堆,每个堆只包含同样大小的对象,则可以避免上述现象。

    (3)使内存访问局部化

      ①如果把内存的访问局限在一个较小的地址区间内,可以减少内存和磁盘之间的页面换入和换出操作,提高性能。

      ②程序设计时,可以在同一个堆中相邻的内存地址分配NODE对象,这样就可以尽可能把多个NODE对象放在同一个物理内存页中,遍历链表时,就可以减少访问太多的不同页面而导致页面的换入换出的交换操作。

      ③如果在同一个堆中分配了NODE和BRANCH对象且各个NODE对象都不相邻,极端情况下,设每个内存页只有一个NODE对象和BRANCH对象,这时遍历链表时可能会导致访问每个NODE都会引起页面错误,效率极低。

    (4)避免线程同步的开销

      ①默认下对堆的访问是依次进行的。这样即使在同一时刻有多个线程要访问堆,也不会出现数据被破坏的情况,但堆函数要执行额外的堆的线程安全性保护的代码。

      ②如果创建一个新的堆,且该堆只会有一个线程会对其访问,这里可以给堆指定HEAP_NO_SERIALIZE属性,这样堆函数就不需执行额外的保护代码,从而提高了速度。

    (5)快速释放

      把一些数据结构存入一个专门的堆时,在不需要这些结构时,可以不必显式地释放堆中的每个内存块。而是可以直接销毁整个堆。

    18.3 如何创建额外的堆

    (1)创建私有堆的函数:HANDLE HeapCreate(fdwOptions,dwInitialSize,dwMaximumSize)

    参数

    含义

    DWORD fdwOptions

    新堆的可选属性,可以是下列的组合

    ①0:默认

    ②HEAP_NO_SERIALIZE:非独占地访问堆,不需要串行化。不指定该标志时,就是默认的独占访问。(该标志是线程不安全,不建议使用!),

    ③HEAP_GENERATE_EXCEPTIONS:当在堆中分配或重新分配内存块失败时,执出一个异常,用于通知应用程序有错误发生。

    ③HEAP_CREATE_ENABLE_EXCUTE:在堆中存放可执行代码,但需要在“数据执行保护”选项中启用DEP(详细可参考第13章)

    SIZE_T dwInitialSize

    初始化时,要调拨给堆的字节数。函数会将该值向上取整到CPU页面大小的整数倍。

    SIZE_T dwMaximumSize

    堆所能增长到的最大大小。如果设为0,表示没有上限。从堆中分配内存会使堆用尺所有的物理存储器为止。

    返回值

    返回新创建的私有堆的句柄。

    备注:①默认下,调用Heap*函数,如果操作系统发现堆被破坏(如写内存时越界),这时在调试运行时会引发一个断言,但没有其他信息。

    ②可以在堆管理器中进行设置,一旦Heap*函数发现堆破坏,就抛出一个异常,方法如下:

    HeapSetInformation(NULL,HeapEnableTerminationOnCorruption,NULL,0)。这个策略会应用到进程中所有的堆。而且一旦为进程所有的堆启用这个特性,就再也无法禁用它了。

    (2)从堆中分配内存块:PVOID HeapAlloc(hHeap,fdwFlags,dwBytes)

    参数

    含义

    HANDLE hHeap

    堆的句柄,表示要从哪个堆中分配内存。

    DWORD fdwFlags

    HEAP_ZERO_MEMORY:把内存块内容清零

    HEAP_GENERATE_EXECPTIONS:如果堆中没有足够内存,函数会抛出异常。如果内存不足时,会抛出STATUS_NO_MEMORY异常;如果堆被破坏或传入的参数不正确时,会抛出STATUS_ACCESS_VIOLATION异常。

    HEAP_NO_SERIALIZE:强制系统不要把这次的HeapAlloc调用与其他线程对同一个堆的访问串行化。(可能破坏堆的完整性,慎用!)

    SIZE_T dwBytes

    要从堆中分配多少个字节

    返回值

    返回分配到的内存地址。

    备注:①调用HeapCreate时可以传入HEAP_GENERATE_EXCEPTIONS标志,这时HeapAlloc可以不指定。如果在创建堆的时候没有指定这个标志,而是在调用HeapAlloc时指定的话,则这个标志只影响当前这次调用,而不会影响在这个堆上所有其他对HeapAlloc函数的调用。

    ②在分配大块内存(1MB或更多)时,应避免使用堆函数,建议使用VirtualAlloc函数。

    ③如果分配大小不同的内存块,可能很容易产生地址空间碎片化,我们可以强制系统在分配内存时使用一种低碎片堆的算法。(如果堆使用的是HEAP_NO_SERIALIZE创建,下列调用会失败)

        ULONG iValue = 2;

    HeapSetInformation(hHeap,HeapCompatibilityInformation,&iVlaue,sizeof(iValue));

    (3)调整内存块的大小:PVOID HeapReAlloc(hHeap,fdwFlags,pvMem,dwBytes)

    参数

    含义

    HANDLE hHeap

    需要调整大小的内存块所在的

    DWORD fdwFlags

    HEAP_GENERATE_EXCEPTIONS、HEAP_NO_SERIALIZE

    HEAP_ZERO_MEMORY:增大内存块是,额外的字节清0

    HEAP_REALLOC_IN_PLACE_ONLY:增大内存块时,不会移动内存块。(这对于链表或树来说,有时很重要,因为节点可能包含指向当前节点指针,当被移到其他堆的其他地方时会破坏链表或树的完整性。

    PVOID pvMem

    要调整大小的内存块的当前地址

    SIZE_T dwBytes

    新的内存块的大小

    返回值

    返回新创建新的内存块的地址或NULL。

    备注:如果不需要移动内存块的前提下增大内存块或把内存块减小时,函数会返回原来内存块的地址。如果必须移动内存块,函数会返回一个新的地址。

    (4)获得内存块的大小:SIZE_T HeapSize(hHeap,fdwFlags,pvMem);//其中hHeap用来标识堆,参数pvMem表示内存块的地址。fdwFlags为0或HEAP_NO_SERIALIZE。

    (5)释放内存块:BOOL HeapFree(hHeap,fdwFlags,pvMem);//各参数与HeapSize含义一样。这个函数可能会使堆管理器撤销一些己经调拨的物理存储器,但这并不是一定的。

    (6)销毁堆:BOOL HeapDestroy(hHeap);

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

      ②进程的默认堆在进程结束时会自动销毁,如果手动调用来销毁则函数的调用会被忽略并返回FALSE

      ③其他私有堆在不用时,可以手动调用该函数来销毁。如果没有被销毁,在进程结束以后系统会替我们销毁。

    【Heap程序】演示如何使用堆

    #include <tchar.h>
    #include <windows.h>
    #include <time.h>
    #include <stdio.h>
    
    void PrintArray(float fArray[], int iCnt){
        for (int i = 0; i < iCnt;i++){
            printf("[%03d]=%0.0f	",i,fArray[i] );
        }
    }
    
    int _tmain(){
        srand((unsigned int)time(NULL));
    
        //在进程默认堆中申请内存
        HANDLE hHeap = GetProcessHeap();
        const int iCnt = 200;
    
        //申请fArray内存块
        float* fArray = (float*)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, iCnt*sizeof(float));
    
        for (int i = 0; i < iCnt;i++){
            fArray[i] = 1.0f*rand();
        }
        //扩大fArray内存块的大小
        fArray = (float*)HeapReAlloc(hHeap, HEAP_ZERO_MEMORY, fArray, 2 * iCnt*sizeof(float));
        for (int i = iCnt; i < 2 * iCnt;i++){
            fArray[i] = 1.0f*rand();
        }
        //打印内存块(数组)的内容
        printf("Default Heap:
    ");
        PrintArray(fArray, 2 * iCnt);
    
        HeapFree(hHeap, 0, fArray);//释放内存块
    
        //创建私有堆
        hHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 0, 0);
        
        //申请内存块
        fArray = (float*)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, iCnt*sizeof(float));
        for (int i = 0; i < iCnt;i++){
            fArray[i] = 1.0f*rand();
        }
        
        //扩大fArray内存块的大小
        fArray = (float*)HeapReAlloc(hHeap, HEAP_ZERO_MEMORY, fArray, 2 * iCnt*sizeof(float));
        for (int i = iCnt; i < 2 * iCnt; i++){
            fArray[i] = 1.0f*rand();
        }
    
        //打印内存块(数组)的内容
        printf("Additional Heap:
    ");
        PrintArray(fArray, 2 * iCnt);
    
        HeapFree(hHeap,0,fArray);
        HeapDestroy(hHeap);  //私有堆,要手动销毁
    
        return 0;
    
    }

    【CSomeClass程序】在C++中使用堆

    #include <windows.h>
    #include <stdio.h>
    
    /*/////////////////////////////////////////////////////////////////////////
    演示如何利用C++来使用的创建和销毁
    //主要功能:
    1、让同一个类的实例在同一个堆中分配空间
    2、当使用计数s_uNumAllocsInHeap为0时,会自动销毁堆
    
    //可能的问题
    1、当考虑继承时,如果新类继承了new和delete操作符,则派生类也会从同一个堆中
       分配内存。这种情况有时是我们希望的,但也可能不是我们希望的,因为对象的大
       小可能相差非常大,那就可能在堆中造成严重的碎片
    2、如果想在派生类中使用一个单独的堆,就可以在派生类中增加一组s_hHeap和
       s_uNumAllocsInHeap变量,同时把new和delete操作符的代码复制过去。这样就不会调用
       基类的操作符了。
    /////////////////////////////////////////////////////////////////////////*/
    class CSomeClass{
    private:
        static HANDLE s_hHeap; //保存堆句柄,让同一个类的实例在同一个堆中分配空间
        static UINT s_uNumAllocsInHeap;//计数器,用来记录从堆中分配了多少个
                                       //CSomeClass对象,用来控制销毁堆
        //...其他私有变量和成员函数
        int iTest; //用于测试目的
    public:
        void* operator new(size_t size); //重载new操作符
        void  operator delete(void* p);  //重载delete操作符
    
        //...其他的全局变量和函数
        //以下函数用于测试目的
        int GetTestValue(){ return iTest; }
        void SetTestValue(const int iValue){ iTest = iValue; }
        int GetCount(){ return s_uNumAllocsInHeap; }
    };
    
    HANDLE CSomeClass::s_hHeap = NULL; //静态变量初始化
    UINT CSomeClass::s_uNumAllocsInHeap = 0;
    
    //new操作符中的size编译器会帮我们传入,等于sizeof(CSomeClass)
    void*  CSomeClass::operator new(size_t size){
        if (s_hHeap == NULL){
            //如果堆不存在,则创建
            //1、HEAP_NO_SERIALIZE表示这个类不支持多线程的
            //2、第2个参数为0,而不传入size。因为在这个堆中
            //   可能会创建多个类的对象和一些额外的空间。
            s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);
            if (s_hHeap == NULL)
                return NULL;
        }
    
        //如果堆己经存在,则为对象分配空间大小
        void* p = HeapAlloc(s_hHeap, 0, size);
    
        if (p != NULL){
            s_uNumAllocsInHeap++; //计数加1    
        }
    
        return p;
    }
    
    void CSomeClass::operator delete(void* p){
        if (HeapFree(s_hHeap,0,p)){
            s_uNumAllocsInHeap--; //删除对象成功
        }
    
        if (s_uNumAllocsInHeap == 0){
            //堆中己经没有类的对象了
            if (HeapDestroy(s_hHeap)){
                s_hHeap = NULL; //设为NULL,下次再创建对象时会
                                //重新创建一个堆
            }
        }
    }
    
    int main(){    
        //编译器编译下面一行代码时,会检查到CSomeClass重载了new操作符
        //就会生成代码来调用这个成员函数,所以会执行函数里堆的创建等
        //操作,如果没有重载new,编译器将生成代码来调用C++标准的new操作符
        CSomeClass* pSome = new CSomeClass;
        
        pSome->SetTestValue(100);
        printf("在堆中分配了%d类实例对象
    ", pSome->GetCount());
        printf("sizeof(CSomeClass)=%d,iTest = %d
    ", sizeof(CSomeClass),pSome->GetTestValue());
       
        delete pSome;
        return 0;
    }

    18.4 其他堆函数

    (1)获取进程中所有的堆(含默认堆):GetProcessHeaps

      ①函数原型:DWORD GetProcessHeaps(dwNumHeaps,phHeaps);

      ②参数说明:dwHeaps要获得的堆数目,phHeaps数组用来接收返回的堆句柄。可用两个调用方法得到堆的数目,第1次dwCount = GetProcessHeaps(0, NULL);其中的dwCount就是进程中堆的数量。

    (2)验证堆的完整性:HeapValidate函数

      ①函数原型:BOOL HeapValidate(hHeap,fdwFlags,pvMem)

      ②参数说明:fdwFlags只能传入0或HEAP_NO_SERIALIZE。如果pvMem指定为NULL,则函数会遍历堆中各个内存块,只有要一个内存块被破坏,就会返回FALSE。如果pvMem指定一个内存块的地址,则只检查这个内存块。

    (3)合并闲置的内存块:(会同时撤销调拨给这些内存块的物理存储器):HeapCompact

      ①函数原型:UINT HeapCompact(hHeap,fdwFlags);

      ②参数说明:fdwFlags只能传0或HEAP_NO_SERIALIZE标志。

    (4)锁定和解锁堆:HeapLockHeapUnlock

      ①这两个函数用于线程同步,必须配对使用。

      ②HeapLock锁定堆时,其他线程调用堆函数(并且操作同一个堆)时将被系统挂起。HeapUnlock唤醒这些线程。

      ③为了确保对堆的访问是依次进行的,HeapAlloc、HeapSize、HeapFree这些函数的内部调用了HeapLock和HeapUnlock。所以我们不需要自己去调用HeapLock和HeapUnlock。

    (5)遍历堆:HeapWalk(hHeap,pHeapEntry);

     

      ①遍历时,需多次调用这个函数,而且调用时必须分配并初始化一个PROCESS_HEAP_ENTRY结构(如上图所示),并将地址传给HeapWalk。

      ②开始枚举堆的内存块时,必须把该结构体的lpData成员设为NULL,这向HeapWalk表明要初始化结构体中的成员。

      ③每次成功调用heapWalk之后,可以查看结构成员来了解内存块的情况。

      ④要得到下一个内存块,必须再次调用HeapWalk,并传入和一上次调用时相同的堆句柄和PROCESS_HEAP_ENTRY结构体的地址。

      ⑤当HeapWalk返回FALSE时,表示堆中己经没有更多的内存块了。

      ⑥可以在HeapWalk循环的外部调用HeapLock和HeapUnlock函数,这个在遍历堆的时候,其他线程便无法操作这个堆。

     【HeapWalk程序】遍历进程中所有的堆,并显示堆中的内存分配信息

    #include <tchar.h>
    #include <windows.h>
    #include <time.h>
    #include <locale.h>
    
    int _tmain(){
        _tsetlocale(LC_ALL, _T("chs"));
    
        srand((unsigned int)time(NULL));
        const int iCnt = 100;
        HANDLE hHeap = GetProcessHeap();
        void* pMem[iCnt];
        ZeroMemory(pMem, iCnt* sizeof(void*));
    
        //分配100个内存块,大小随机
        for (int i = 0; i < iCnt;i++){
            pMem[i] = HeapAlloc(hHeap, 0, 50/*rand() % iCnt*/);
        }
    
        PROCESS_HEAP_ENTRY phe = {};
        HeapLock(hHeap); //锁定堆
    
        int iBlock = 0;
        //遍历进程默认堆
        while (HeapWalk(hHeap,&phe)){
            ++iBlock;
        }
        HeapUnlock(hHeap);//释放堆
    
        _tprintf(_T("进程默认堆中共有%d个内存块
    "),iBlock);
        //for (int i = 0; i < iCnt;i++){
        //    HeapValidate(hHeap, 0, pMem[i]);
        //
        //    _tprintf(_T("pMem[%d]=0x%08X is %s BlockSize=%d Bytes
    "), i, pMem[i], 
        //                HeapValidate(hHeap, 0, pMem[i])? TEXT("Valid"):TEXT("InValid"),
        //                HeapSize(hHeap, 0, pMem[i]));
        //    HeapFree(hHeap, 0, pMem[i]);
        //}
    
        //下面的代码演示如何遍历一个进程中的所有的堆
        DWORD dwHeapCnt = 0;
        PHANDLE pHArray = NULL;
        dwHeapCnt = GetProcessHeaps(0, NULL);
        if (dwHeapCnt>0){
            pHArray = (PHANDLE)HeapAlloc(GetProcessHeap(), 0, dwHeapCnt*sizeof(HANDLE));
            GetProcessHeaps(dwHeapCnt, pHArray);
            for (DWORD i = 0; i < dwHeapCnt;i++){
                HeapLock(pHArray[i]);
                iBlock = 0;
                ZeroMemory(&phe, sizeof(PROCESS_HEAP_ENTRY));
                _tprintf(_T("堆[%p]中的内存分配情况:
    "), pHArray[i]);
                while (HeapWalk(pHArray[i],&phe)){
                    ++iBlock;
                    
                    _tprintf(_T("	块[%p]的信息,这里省略...
    "),phe.lpData);
                    //显示块信息,这里省略。。。
                    //DisplayHeapInfo(phe); //每个内存块的信息放在phe的结构体中
                }
                _tprintf(_T("堆[%p]中共有%d个内存块
    
    "),pHArray[i],iBlock);
                HeapUnlock(pHArray[i]);
            }
            HeapFree(GetProcessHeap(), 0, pHArray);
        }
        _tprintf(_T("本进程中共有%d个堆
    
    "), dwHeapCnt);
        return 0;
    }
  • 相关阅读:
    手游页游和端游的服务端的架构与区别
    TiKV 源码解析系列——如何使用 Raft
    TiKV 源码解析系列
    三篇文章了解 TiDB 技术内幕 —— 谈调度
    三篇文章了解 TiDB 技术内幕——说计算
    三篇文章了解 TiDB 技术内幕——说存储
    TiDB 源码阅读系列文章(一)序
    【合集】TiDB 源码阅读系列文章
    9个offer,12家公司,35场面试,从微软到谷歌,应届计算机毕业生的2012求职之路
    python datetime和unix时间戳之间相互转换
  • 原文地址:https://www.cnblogs.com/5iedu/p/4947054.html
Copyright © 2020-2023  润新知