• 回炉重造之重读Windows核心编程-018-堆栈


    第18章 堆栈

    相对于虚拟内存和内存映射文件,堆栈的优点是可以忽略分配粒度和页边界之类的问题,集中精力处理任务;而缺点就是分配和释放的速度慢,而且无法直接控制物理存储器的提交和回放。

    堆栈就是进程保留地址空间的一个区域。堆栈管理器在开始的时候只把一部分的页面提交给堆栈,在堆栈中进行越来越多的内存分配时,堆栈管理器才将更多的更多的页面提交给堆栈。当释放堆栈中的内存块时,堆栈管理器将回收这些物理存储器。

    值得一提的是Windows2000及以后更加注重于速度了,只有在一段时间内的时间内都会占据内存管理器。只有在一段时间后页面不在被使用,才会将页面返回给页文件。Windows常常进行适应性测试,以确定在大部分时间内最适合的规则。也就是说Windows的内存管理规则是相对灵活的,不过你也可以用虚拟内存函数(也就是VirtualAlloc和VirtualFree)控制这些规则。

    18.1 进程的默认堆栈

    这个默认的堆栈在进程的初始化的时候创建,大小的是默认的1MB。可以使用/HEAP链接开关修改这个默认值。要注意的是,DLL没有与之相关的堆栈,不能用/HEAP开关。

    /HEAP:reserve[或者commit]
    

    这个默认堆栈还是很有存在感的。例如Windows的核心函数都是用Unicode字符和字符串执行它们全部的操作,如果调用ANSI的版本,那么ANSI版本就必须将ANSI字符串转换成Unicode字符串,单后调用同一个版本的函数。而这个转换的过程需要的内存,就是从进程的默认堆栈中分配的。

    还有在多线程环境下,系统应该保证每次只能有一个线程访问默认堆栈。这样的话,效率会受到一定的影响。而且杯具的是,这种设置没有办法改变。

    一个进程可以拥有多的堆栈,它们在进程的生命周期中创建和销毁。但是默认堆栈是在进程开始前就创建的,在进程时自动销毁,不能手动销毁它。这些堆栈也有自己的句柄来标识,可以通过下面的函数获取你的进程的默认堆栈的句柄。

    HANDLE GetProcessHeap();
    

    18.2 为什么要创建辅助堆栈

    由于下列原因,你可能想要在自己的应用程序中创建一些辅助堆栈:

    • 保护组件
    • 更加有效地进行内存管理
    • 进行本地访问
    • 减少进程间同步的开销
    • 迅速释放

    18.2.1 保护组件

    由于应用程序中永远有各种各样的数据结构的存在,必然有不同的对象需要用不同的数据结构来管理,这时候就需要用到辅助堆栈了,有了它不同的组件之间的内存有了清晰的界线,在查找指定对象和定位问题的时候就方便多了。

    18.2.2 更加有效地进行内存管理

    通过在堆栈中分配同样大小的对象,就可以更加有效地管理堆栈。如果不进行内存管理,就会产生很多内存的碎片,例如一个或者多个对象并不总是能填满整个页面,这就可能产生很多内存的碎片。而且管理同样大小的对象的时候,销毁一个对象又新增一个对象的时候,新的对象就可以刚好填进旧的位置。

    18.2.3 进行本地访问

    在RAM和页文件之间进行页面交换的时候,系统的运行是会受到很严重的影响的。因此如果只是访问一段小范围内的内存,这样的交换就显得不是那么的必要。所以如果有些数据将被访问的话,尽量将其放在靠近的位置上(一个堆栈中),这样一个页面就可以囊括这些数据。那么有些情况下,例如对树进行遍历,如果树的所有节点都在一个页面上,那么遍历起来就不会涉及到页面的交换了。这样不仅对效率有可观的提升,也避免了在树的一些节点在其他页面上的情况下的全盘遍历发送的缺页异常。

    18.2.4 减少进程间同步的开销

    正如上面提的那样,堆栈被顺序进行访问,使得即使有多个线程死如访问堆栈,也不会对堆栈里的数据造成破坏。不过堆栈函数就要负担起线程的安全性,去面对可能有的多线程访问。

    18.2.5 迅速释放堆栈

    最后就是,堆栈管理器可以直接释放整个堆栈,而不是显示释放堆栈中的每个内存块。

    18.3 如何创建辅助堆栈

    可以在进程中创建辅助堆栈,方法是调用函数:

    HANDLE HeapCreate(
    	DWORD fdwOptions,
    	SIZE_T dwInitialSize,
    	SIZE_T dwMaximumSize));
    

    第一个参数fdwOptions用于规定在堆栈上的操作,可以设置0、HEAP_NO_SERIALIZE、HEAP_GENERATE_EXCEPTIONS或者是这两个标志的组合。
    接着,当试图从堆栈中分配一个内存映射时,HeapAlloc函数必须要做一下操作:

    1. 遍历分配的的和释放的内存看的链接表
    2. 寻找一个空闲内存块的地址
    3. 通过将空闲内控块标记为“已分配”,分配新的内存块
    4. 将新内存块添加给内存块链接表

    HEAP_NO_SERIALIZE标志应该避免被使用。假定有2个线程都要访问堆栈,首先是线程1先拿到访问权,执行了上面提上的第1步和第2步。在正要执行第3步标记内存块为“已分配“前,线程2抢占了运行,同样执行了第1步和第2步,并且也执行了第3步,把内存块标记为了”已分配“。这样线程2就获得了一个空闲内存块的地址。
    现在线程1和2都认为自己得到了一个空闲的内存,并且都给内存块打上了”已分配“的标记。这样的错误怎么跟踪?它是不会即时就暴露出来的,而是在你想象不到的时候。而且这样也引发了以下的问题:

    • 这个内存块被破坏,在试图释放或者分配内存块之前不会被发现。
    • 2个线程共享一个内存块,内存块的数据的合法性无法保证。
    • 其中一个线程如果将这个内存块释放了,那么另一个线程再访问它就破坏堆栈了。

    为了避免这种状况,HEAP_NO_SERIALIZE标志就不能使用了。除非你的程序能够规避上面提到的问题。

    另一个标志HEAP_GENERATE_EXCEPTIONS让系统在用函数HeapCreate的时候,如果失败就会产生一个异常,让应用程序有机会处理。细节将在和异常有关的章节中提及。

    参数dwInitialSize指明最初提交给堆栈的字节数会被圆整为一个页的大小的倍数。参数dwMaximumSize指明堆栈可以扩展到的最大值(也就是系统能为堆栈保留的地址空间的最大数量)。如果dwMaximumSize大于零,那么你创建的堆栈具有最大值。如果尝试分配的内存块的大小超过最大值,那么这种尝试就会失败。

    如果dwMaximumSize的值等于0,那么可以创建一个能够扩展的堆栈,它没有内在的限制。它可以不断分配,直到物理存储器用完为止。

    18.3.1 从堆栈中分配内存块

    调用函数就可以:

    PVOID HeapAlloc(
    	HANDLE hHeap,
    	DWORD  fdwFlags,
      SIZE_T dwBytes);
    

    参数hHeap是HeapCreate返回的句柄,fdwFlags参数设定从堆栈中分配的内存块的字节数,dwBytes参数指明如何分配。支持的标志有3个:、和。

    参数 意义
    HEAP_ZERO_MEMORY 用0填写新内存块的内容
    HEAP_GENERATE_EXCEPTION 在函数失败的时候用来引发一个软件异常条件。如果HeapCreate中设定过这个标记,这里不必再设置。另外如果HeapCreate没有这个标记,那么这个标记对于HeapAlloc函数来说就是一次性的了。
    HEAP_NO_SERIALIZE 不建议使用
    标记 含义
    STATUS_NO_MEMORY 由于内存不足,分配内存块的尝试失败
    STATUS_ACCESS_VIOLATION 堆栈被破坏,或者参数不正确,分配内存尝试失败

    18.3.2 改变内存块的大小

    分配内存时对内存块的把控并不是一帆风顺的,已经分配的内存块的大小不可能适应变化的需求。所以需要函数可以修改内存块的大小:

    PVOID HeapReAlloc(
    	HANDLE hHeap,
    	DWORD  fdwFlags,
    	PVOID  pvMem,
    	SIZE_T dwBytes);
    

    hHeap参数不用多说。而fdwFlags参数可以是4个值:

    含义
    HEAP_GENARATE_EXCEPTIONS 与HeapAllc相同
    HEAP_NO_SERIALIZE 与HeapAllc相同
    HEAP_ZERO_MEMORY 在扩大内存块的情况下生效,新内存块置零
    HEAP_REALLOC_IN_PLACE_ONLY

    18.3.3 了解内存块的大小

    当内存块分配后,可以用HeapSize函数来检索内块的实际大小:

    SIZE_T HeapSize(
    	HANDLE hHeap,
    	DWORD  fdwFlags,
    	LPCVOID pvMem);
    

    参数hHeap标识堆栈,pvMem参数是内存的地址,fdwFlags可以是0或者HEAP_NO_SERIALIZE。

    18.3.4 释放内存块

    当不再需要内存块的时候,可以用HeapFree函数将它释放:

    BOOL HeapFree(
    	HANDLE hHeap,
    	DWORD  fdwFlags,
    	PVOID  pvMem);
    

    参数hHeap标识堆栈,pvMem参数是内存的地址,fdwFlags可以是0,或者HEAP_NO_SERIALIZE。使用这个函数可以让堆栈管理器回收某些物理存储器,但是是否成功就不确定了。

    18.3.5 撤销堆栈

    释放内存块,调用这个函数就可以:

    BOOL HeapDestroy(HANDLE hHeap);
    

    这个函数将释放堆栈中包含的所有内存块。如果函数成功,返回值是TRUE。 值得庆幸的是,如果在进程完全终止之前没有显式地撤销堆栈,系统会为你撤销它。但是如果是线程创建的堆栈,线程在终止的时候是不会被撤销的。
    在进程的完全终止运行之前,系统不允许进程的默认堆栈被撤销。此时即使调用了这个函数,系统也会忽略掉的。

    18.3.6 用C++程序来使用堆栈

    使用堆栈最好的方法之一是将堆栈纳入现有的C++程序。在C++中,你可以用new操作符,而不是调用C运行时例程malloc,就可以执行类对象的分配操作。然后当我们不再需要这个对象的时候,再使用delete操作符,而不是用C运行时的例程free将它释放。例如下面的代码:

    CSomeClass* pcls = new CSomeClass;
    

    当完成对已分配对象的使用后,可以调用delete操作符将它撤销:

    delete pcls;
    

    当C++编译器检测到这行代码时,它首先查看CSomeClass类是否包含new操作符的成员函数,如果包含,那么编译器就生成调用该函数的代码。如果编译器没有找到new操作符对应的函数,那么编译器将生成调用标准C++的new操作符函数的代码。
    通过new和delete操作符,就可以轻松加愉快地利用堆栈函数。

    现在我们来看一些代码:

    class CSomeClass {
    private:
      static HANDLE s_hHeap;
      static UINT s_uNumAllocsInHeap;
      // other member data
    public:
      void* operator new (size_t size);
      void  operator delete (void* p);
      // other member foo
    };
    

    这个代码段中,两个成员变量s_hHeap和s_uNumAllocsInHeap作为静态变量,被这个类CSomeClass的所有实例共享相同的变量,C++不会为已经创建过的的这个类的每个实例分配单独的两个静态变量。
    s_hHeap参数是一个句柄,包含了为这个类创建的对象所在堆栈的句柄。参数s_uNumAllocsInHeap则是一个计数器,用于计算堆栈中已经分配了多少个这个类的对象。每次在堆栈中分配一个新的对象时,s_uNumAllocsInHeap参数就递增,释放一个对象时计数器就递减。当s_uNumAllocsInHeap为0时,说明堆栈不再被需要,并被释放。这样的操作类似于下面的代码:

    HANDLE  CSomeClass::s_Heap = 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 object.
      void* p = HeapAlloc(s_Heap, 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 object.
      return(p);
    }
    

    首先初始化了两个静态的变量,分别为NULL和0。

    C++的new操作符接收一个参数,即size,指明CSomeClass的对象所需要的字节数。这里只需要判断类里的静态遍历s_Heap有没有没初始化过就可以决定是否创建新的堆栈。

    这里使用了HEAP_NO_SERIALIZE标志。这是因为CSomeClass这个类中不需要有多线程安全性。

    HeapCreate的另外两个参数被设置为0,表明堆栈的初始大小和最大值。第一个0表示没有初始化的值,第二个0则表示堆栈可以更具需要进行扩展,这里是可以根据需要改变的。

    这样一旦堆栈创建完成,就可以使用HeapAlloc函数从该堆栈中分配新的CSomeClass对象。一个参数是堆栈的句柄,第二个参数是对象的大小。HeapAlloc返回分配的内存块的地址。

    整个过程成功之后,计数器需要递增。

    最后是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;
        }
      }
    }
    

    第一步,先调用HeapFree,释放参数p指定的对象,如果成功,那么计数器递减。
    第二步,判断计数器是否已经到0,如果到0,就调用HeapDestroy销毁堆栈。
    最后在把s_hHeap设置为NULL。

    这个例子展示了使用多个堆栈的简便方法。这个例子很容易建立,并且可以纳入若干
    个类中。不过应该对继承性问题有所考虑。如果你用 CSomeClass类作为一个基类,派生一个新类,那么这个新类就可以继承 CSomeClass的new和delete操作符。这个新类也可以继承CSomeClass的堆栈,这意味着当 new操作符用于派生类时,该派生类对象的内存将从CSomeClass使用的同一个堆栈中分配。根据具体情况,你也许希望这样,也许不希望这样。如果对象的大小差别很大,建立的堆栈环境可能使你的堆栈变得支离破碎。正如本章前面部分中的“组件保护”和“更加有效地进行内存管理”两节所说的那样,你可能更加难以跟踪代码中的错误

    18.4 其他堆栈函数

    就是Platform SDK中提到的了,包括Heap32First、Heap32Next、Heap32ListFirst和Heap32ListNext。

    由于进程空间中可以存在多个堆栈,因此可以使用GetProcessHeaps来获取现有堆栈的句柄。

    DWORD GetProcessHeaps(
    	DWORD dwNumHeaps,
    	PHANDLE pHeaps);
    

    若要调用GetProcessHeaps函数,还得要先分配一个HANDLE数组,在调用下面的函数:

    HANDLE hHeaps[32];
    DWORD dwHeaps = GetProcessHeaps(32, hHeaps);
    if (dwHeaps > 32) {
      // no enough
    }
    else {
      ...
    }
    

    要注意,进程的默认堆栈也包含在其中。

    函数HeapValidate用于验证堆栈的完整性。

    BOOL HeapValidate(
    	HANDLE hHeap,
    	DWORD  fdwFlags,
    	LPCVOID pvMem);
    

    若要合并地址中的空闲内存块并收回不包含已经分配的地址内存块的存储器页面,可以调用下面的函数(fdwFlags参数一般传递0):

    UINT HeapCompact(
    	HANDLE hHeap,
    	DWORD  fdwFlags);
    

    下面两个函数是应对线程同步的。当HeapLock被调用时,调用线程将成为堆栈的所有者,

    BOOL Heaplock(HANDLE hHeap);
    BOOL HeapUnlock(HANDLE hHeap);
    

    HeapAlloc、HeapSize和HeapFree等函数在内部调用HeapLock和HeapUnLock函数来确保对堆栈的访问能顺序进行。自己调用它们是不常见的。

    BOOL HeapWalk(
    	HANDLE hMem,
    	PROCESS_HEAP_ENTRY pHeadEntry);
    

    最后一个函数只用于调试目的。它使你遍历堆栈的内容。可以多次调用该函数。每次调用该函数时,将传递必须分配和初始化的PROCESS_HEAP_ENTRY结构的地址:

    typedef struct _PROCESS_HEAP_ENTRY {
      PVOID lpData; 
      DWORD cbData;
      BYTE  cbOverhead;
      BYTE  iRegionIndes;
      WORD  wFlags;
      union {
        struct {
          HANDLE hMem;
          DWORD  dwReserved[3];
        } Block;
        struct {
          DWORD dwCommitedSize;
          DWORD dwUnCommitedSize;
          LPVOID lpFirstBlock;
          LPVOID lpLastBlock;
        } Region;  
      };
    }PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY;
    

    首次调用HeapWalk。这个函数只用于调试目的,能让你遍历堆栈的内容。需要多次调用这个函数,每次调用时,必须传递已经初始化的PROCESS_HEAP_ENTRY结构的对象或指针。

    痛苦苦苦苦苦苦苦苦苦并快乐着
  • 相关阅读:
    Kaggle 神器 xgboost
    改善代码可测性的若干技巧
    IDEA 代码生成插件 CodeMaker
    Elasticsearch 使用中文分词
    Java性能调优的11个实用技巧
    Lucene 快速入门
    Java中一个字符用unicode编码为什么不是两字节
    lucene 的评分机制
    面向对象设计的 10 条戒律
    2019.10.23-最长全1串(双指针)
  • 原文地址:https://www.cnblogs.com/leoTsou/p/13569238.html
Copyright © 2020-2023  润新知