成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

談一談Windows中的堆

系統(tǒng) Windows
如果在Windows中編程應(yīng)該了解一些Windows的內(nèi)存管理,而堆(Heap)也屬于內(nèi)存管理的一部分。這篇文章對(duì)你理解Windows內(nèi)存分配的基本原理和調(diào)試堆內(nèi)存問(wèn)題或許會(huì)有所幫助。

[[413851]]

本文轉(zhuǎn)載自微信公眾號(hào)「一個(gè)程序員的修煉之路」,作者河邊一枝柳。轉(zhuǎn)載本文請(qǐng)聯(lián)系一個(gè)程序員的修煉之路公眾號(hào)。

如果在Windows中編程應(yīng)該了解一些Windows的內(nèi)存管理,而堆(Heap)也屬于內(nèi)存管理的一部分。這篇文章對(duì)你理解Windows內(nèi)存分配的基本原理和調(diào)試堆內(nèi)存問(wèn)題或許會(huì)有所幫助。

Windows Heap概述

下圖參考<<Windows高級(jí)調(diào)試>>所畫(huà),并做了一些小小的修改。可以看出來(lái)程序中對(duì)堆的直接操作主要有三種:

  1. 進(jìn)程默認(rèn)堆。每個(gè)進(jìn)程啟動(dòng)的時(shí)候系統(tǒng)會(huì)創(chuàng)建一個(gè)默認(rèn)堆。比如LocalAlloc或者GlobalAlloc也是從進(jìn)程默認(rèn)堆上分配內(nèi)存。你也可以使用GetProcessHeap獲取進(jìn)程默認(rèn)堆的句柄,然后根據(jù)用這個(gè)句柄去調(diào)用HeapAlloc達(dá)到在系統(tǒng)默認(rèn)堆上分配內(nèi)存的效果。
  2. C++編程中常用的是malloc和new去申請(qǐng)內(nèi)存,這些由CRT庫(kù)提供方法。而根據(jù)查看在VS2010之前(包含),CRT庫(kù)會(huì)使用HeapCreate去創(chuàng)建一個(gè)堆,供CRT庫(kù)自己使用。在VS2015以后CRT庫(kù)的實(shí)現(xiàn),并不會(huì)再去創(chuàng)建一個(gè)單獨(dú)的堆,而使用進(jìn)程默認(rèn)堆。 (VS2013的CRT源碼我并未查看,有興趣的可以看看VS2013默認(rèn)的CRT庫(kù)采用的是進(jìn)程默認(rèn)堆還是新建的堆)。
  3. 自建堆。這個(gè)泛指程序通過(guò)HeapCreate去創(chuàng)建的堆,然后利用HeapAlloc等API去操作堆,比如申請(qǐng)空間。

那么堆管理器是通過(guò)調(diào)用虛擬管理器的一些方法進(jìn)行堆管理的實(shí)現(xiàn),比如VirtualAlloc之類(lèi)的函數(shù)。同樣應(yīng)用程序也可以直接使用VirtualAlloc之類(lèi)的函數(shù)對(duì)內(nèi)存進(jìn)行使用。

說(shuō)到這里不免有些生澀,我們就寫(xiě)一個(gè)示例代碼來(lái)看看一個(gè)進(jìn)程的堆情況。

  1. #include <windows.h> 
  2. #include <iostream> 
  3. #include <intsafe.h> 
  4.  
  5. using namespace std; 
  6. const char* GetHeapTypeString(HANDLE pHandle) 
  7.   ULONG ulHeapInfo; 
  8.   HeapQueryInformation(pHandle, 
  9.     HeapCompatibilityInformation, 
  10.     &ulHeapInfo, 
  11.     sizeof(ulHeapInfo), 
  12.     NULL); 
  13.   switch (ulHeapInfo) 
  14.   { 
  15.   case 0: 
  16.     return "Standard"
  17.   case 1: 
  18.     return "Look Aside List"
  19.   case 2: 
  20.     return "Low Fragmentation"
  21.   } 
  22.   return "Unknow type"
  23.  
  24. void PrintAllHeaps() 
  25.  
  26.   DWORD dwNumHeap = GetProcessHeaps(0, NULL); 
  27.   if (dwNumHeap == 0) 
  28.   { 
  29.     cout << "No Heap!" << endl; 
  30.     return
  31.   } 
  32.  
  33.   PHANDLE pHeaps; 
  34.   SIZE_T  uBytes; 
  35.   HRESULT Result = SIZETMult(dwNumHeap, sizeof(*pHeaps), &uBytes); 
  36.   if (Result != S_OK) { 
  37.     return
  38.   } 
  39.  
  40.   pHeaps = (PHANDLE)malloc(uBytes); 
  41.   dwNumHeap = GetProcessHeaps(dwNumHeap, pHeaps); 
  42.   cout << "Process has heaps: " << dwNumHeap << endl; 
  43.   for (int i = 0; i < dwNumHeap; ++i) 
  44.   { 
  45.     cout << "Heap Address: " << pHeaps[i] 
  46.       << ", Heap Type: " << GetHeapTypeString(pHeaps[i]) << endl; 
  47.   } 
  48.  
  49.   return
  50.  
  51. int main() 
  52.   cout << "========================" << endl; 
  53.   PrintAllHeaps(); 
  54.   cout << "========================" << endl; 
  55.  
  56.   HANDLE hDefaultHeap = GetProcessHeap(); 
  57.   cout << "Default Heap: " << hDefaultHeap 
  58.     << ", Heap Type: " << GetHeapTypeString(hDefaultHeap) << endl; 
  59.  
  60.   return 0; 

這是一個(gè)在Win10上運(yùn)行的64位程序輸出的結(jié)果: 這個(gè)進(jìn)程我們并沒(méi)有在main中顯示的創(chuàng)建Heap,我們都知道進(jìn)程在啟動(dòng)的時(shí)候初始化會(huì)創(chuàng)建相關(guān)的資源,其中也包含了堆。這個(gè)進(jìn)程共創(chuàng)建了四個(gè)堆。可以看出來(lái)第一個(gè)堆就是進(jìn)程的默認(rèn)堆,并且是采用的 Low Fragmentation的分配策略的堆。

堆的內(nèi)存分配策略

堆主要有前端分配器和后端分配器,我所理解的前端分配器就是類(lèi)似于緩存一樣,便于快速的查詢所需要的內(nèi)存塊,當(dāng)前端分配器搞不定的時(shí)候,就交給后端分配器。

前端分配器主要分為, 而Windows Vista之后進(jìn)程默認(rèn)堆均采用低碎片前端分配器。

  • 旁視列表 (Look Aside List)
  • 低碎片 (Low Fragmentation)

以下的場(chǎng)景均采用32位的程序進(jìn)行的描述。

前端分配器之旁視列表

旁視列表 (Look Aside List, LAL)是一種老的前端分配器,在Windows XP中使用。

這是一個(gè)連續(xù)的數(shù)組大小為128,每個(gè)元素對(duì)應(yīng)一個(gè)鏈表,因?yàn)槠浯鎯?chǔ)的是整個(gè)Heap塊的大小,那就包含了用戶申請(qǐng)的大小+堆塊元數(shù)據(jù),而這里元數(shù)據(jù)大小為8字節(jié), 而最小分配粒度為8字節(jié)(32位程序),那么最小的堆塊的大小則為16個(gè)字節(jié)。從數(shù)據(jù)1~127,每個(gè)鏈表鎖存儲(chǔ)的堆塊大小按照8字節(jié)粒度增加。

那么當(dāng)用戶申請(qǐng)一個(gè)比如10字節(jié)大小的的內(nèi)存,則在LAL中查找的堆塊大小為18字節(jié)=10字節(jié)+元數(shù)據(jù)8字節(jié),則在表中找到的剛好匹配的堆塊大小為24字節(jié)的節(jié)點(diǎn),并將其從鏈表中刪除。

而當(dāng)用戶釋放內(nèi)存的時(shí)候,也會(huì)優(yōu)先查看前端處理器是否處理,如果處理則將內(nèi)存插入到相應(yīng)的鏈表中。

前端分配器之低碎片

先說(shuō)說(shuō)內(nèi)存碎片我這里簡(jiǎn)要概述下: 如下圖所示假設(shè)一段大的連續(xù)的內(nèi)存被分割為若干個(gè)8字節(jié)的內(nèi)存塊,然后這個(gè)時(shí)候釋放了圖中綠色部分的內(nèi)存塊,那么此時(shí)總共空出了40字節(jié)的內(nèi)存,但想去申請(qǐng)一個(gè)16字節(jié)的內(nèi)存塊,卻無(wú)法申請(qǐng)到一個(gè)連續(xù)的16字節(jié)內(nèi)存塊,從而分配內(nèi)存失敗,這就是內(nèi)存碎片。

所謂的低碎片前端分配器,是將LAL類(lèi)似的數(shù)組中的粒度重新進(jìn)行了劃分:

數(shù)據(jù)Index 堆塊遞增粒度 堆塊字節(jié)范圍
0~31 8 8~256
32~47 16 272~512
112-127 512 8704~16384
 

可以看到同樣的數(shù)組的大小,將其按照不同的粒度劃分,相比較LAL分配的大小粒度逐步增大,到了最后的112-127區(qū)間粒度已經(jīng)增大到了512字節(jié),最大支持的16384。粒度更大的分配有利于緩解內(nèi)存碎片,提高內(nèi)存的使用效率。Windows Vista之后進(jìn)程默認(rèn)堆均采用低碎片前端分配器。

后端分配器

其實(shí)講到前面這部分可能還有一些人云里霧里。那么我們的內(nèi)存到底是怎么劃分出來(lái)的呢?這就是后端分配器要做的事情了。看看后端分配器是如何管理這些內(nèi)存的。

先說(shuō)說(shuō)堆在內(nèi)存中的展現(xiàn)形式,一個(gè)堆主要由若干個(gè)Segment(段)組成,每個(gè)Segment都是一段連續(xù)的空間,然后用雙向鏈表串起來(lái)。而一般情況下,一開(kāi)始只有一個(gè)Segment,然后在這個(gè)Segment上申請(qǐng)空間,叫做Heap Entry(堆塊)。但是這個(gè)Segment可能會(huì)被用完,那就新開(kāi)辟一個(gè)Segment,而且一般新的Segement大小是原先的2倍,如果內(nèi)存不足則不斷的將申請(qǐng)空間減半。這里有個(gè)要注意的就是當(dāng)劃分了一個(gè)新的Segment后比如其空間為1GBytes,那么其真實(shí)的使用的物理內(nèi)存肯定不會(huì)是1GBytes,因?yàn)榇藭r(shí)內(nèi)存還沒(méi)有被應(yīng)用程序申請(qǐng),這個(gè)時(shí)候?qū)嶋H上這個(gè)Segment只是Reserve了這段虛擬地址空間,而當(dāng)真正應(yīng)用程序申請(qǐng)內(nèi)存的時(shí)候,才會(huì)一小部分一小部分的Commit,這個(gè)時(shí)候才會(huì)用到真正的物理存儲(chǔ)空間。

而應(yīng)用程序申請(qǐng)的內(nèi)存在Segment上叫做Entry(塊),他們是連續(xù)的,可以看到一個(gè)塊一般具有:

  • 前置的元數(shù)據(jù): 這里主要存儲(chǔ)有當(dāng)前塊的大小,前一個(gè)塊的大小,當(dāng)前塊的狀態(tài)等。
  • 用戶數(shù)據(jù)區(qū): 這段內(nèi)存才是用戶申請(qǐng)并且使用的內(nèi)存。當(dāng)然這塊數(shù)據(jù)可能比你申請(qǐng)的內(nèi)存要大一些,因?yàn)?2位下面最小的分配粒度是8字節(jié)。這也是為什么有時(shí)候程序有時(shí)候溢出了幾個(gè)字符,好像也沒(méi)有導(dǎo)致程序異常或者崩潰的原因。
  • 后置的元數(shù)據(jù): 這個(gè)一般用于調(diào)試所用。一般發(fā)布的時(shí)候不會(huì)占用這塊空間。

那么哪些塊是可以直接使用的呢?這就涉及到這些塊元數(shù)據(jù)中的狀態(tài),可以表明這個(gè)塊是否被占用,如果是空閑狀態(tài)則可以使用。

后端分配器,不會(huì)傻傻的去遍歷所有的塊的狀態(tài)來(lái)決定是否可以分配吧?這個(gè)時(shí)候就用到了后端分配器的策略。

這個(gè)表有點(diǎn)類(lèi)似于LAL, 只是注意看下這個(gè)index為0的多了一個(gè)list,從小到大排列,可變大小的從大于1016字節(jié)的小于524272字節(jié)的將在這個(gè)鏈表里面存儲(chǔ)。超過(guò)524272字節(jié)將直接通過(guò)VirtualAlloc之類(lèi)的API直接獲取內(nèi)存。

假設(shè)此時(shí)前端堆管理器需要尋找一個(gè)32字節(jié)的堆塊, 后端管理器將如何操作?

這個(gè)時(shí)候請(qǐng)求到了后端分配器,后端分配器假設(shè)也沒(méi)有在這個(gè)表中查找到32字節(jié)的空閑塊,那么將先查找64字節(jié)的空閑塊,如果找到,則將其從列表中移除,然后將其分割為兩個(gè)16字節(jié)的塊, 一個(gè)設(shè)置為占用狀態(tài)返回給應(yīng)用程序,一個(gè)設(shè)置為空閑狀態(tài)插入響應(yīng)的鏈表中。

那如果還沒(méi)有找到呢?那么這個(gè)時(shí)候堆管理器會(huì)從Segment中提交(Commit)更多的內(nèi)存去使用,創(chuàng)建新的塊, 如果當(dāng)前Segment空間也不夠了,那就創(chuàng)建新的Segement

有細(xì)心的同學(xué)可能說(shuō),那前端分配器和后端分配器差不多嗎,這里面有個(gè)很重要的就是,前端分配器鏈表中的塊是屬于占用狀態(tài)的, 而后端分配器鏈表中的塊是屬于空閑狀態(tài)的。

假設(shè)釋放內(nèi)存,該如何操作?

首先要看前端分配器是否處理這個(gè)釋放的塊,比如加入到相應(yīng)的鏈表中去,如果不處理,那么后端分配器將會(huì)查看相鄰的塊是否也是空閑的,如果是空閑狀態(tài),將會(huì)采用塊合并成一個(gè)大的塊,并對(duì)相應(yīng)的后端分配器鏈表進(jìn)行操作。

當(dāng)然了當(dāng)你釋放的內(nèi)存足夠多的時(shí)候,其實(shí)堆管理器也不會(huì)長(zhǎng)期霸占著物理存儲(chǔ)器的空間,也會(huì)在適當(dāng)?shù)那闆r下調(diào)用Decommit操作來(lái)減少物理存儲(chǔ)器的使用。

Windbg查看進(jìn)程中的堆

進(jìn)程堆信息查看

進(jìn)程堆的信息是放在PEB(進(jìn)程環(huán)境塊)中,可以通過(guò)查看PEB相關(guān)的信息, 可以看到當(dāng)前進(jìn)程包含有3個(gè)堆,并且堆的數(shù)組地址為0x77756660

  1. 0:000> dt _PEB @$peb 
  2.    ...... 
  3.    +0x088 NumberOfHeaps    : 3 
  4.    ...... 
  5.    +0x090 ProcessHeaps     : 0x77756660  -> 0x00fa0000 Void 
  6.  
  7.    ...... 

然后我們查看對(duì)應(yīng)的三個(gè)堆的地址,分別為0xfa0000, 0x14b0000和0x2e10000, 而第一個(gè)一般為進(jìn)程的默認(rèn)堆00fa0000。

  1. 0:006> dd 0x77756660 
  2. 77756660  00fa0000 014b0000 02e10000 00000000 
  3. 77756670  00000000 00000000 00000000 00000000 
  4. 77756680  00000000 00000000 00000000 00000000 
  5. 77756690  00000000 00000000 00000000 00000000 
  6. 777566a0  00000000 00000000 00000000 00000000 
  7. 777566b0  00000000 00000000 00000000 00000000 
  8. 777566c0  ffffffff ffffffff 00000000 00000000 
  9. 777566d0  00000000 020007d0 00000000 00000000 

其實(shí)上述步驟Windbg提供了一個(gè)方法可以直接查看概要信息了, 可以看到系統(tǒng)默認(rèn)堆00fa0000為L(zhǎng)FH堆,并且已經(jīng)Reserve了空間為1128K, Commit的內(nèi)存為552K。

  1. 0:000> !heap -s 
  2. ...... 
  3. LFH Key                   : 0x8302caa1 
  4. Termination on corruption : ENABLED 
  5.   Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast  
  6.                     (k)     (k)    (k)     (k) length      blocks cont. heap  
  7. ----------------------------------------------------------------------------- 
  8. 00fa0000 00000002    1128    552   1020    178    21     1    1      0   LFH 
  9. 014b0000 00001002      60     12     60      1     2     1    0      0       
  10. 02e10000 00001002    1188     92   1080      4     4     2    0      0   LFH 
  11. ----------------------------------------------------------------------------- 

可以通過(guò)dt _HEAP 00fa0000命令去查看進(jìn)程默認(rèn)堆的信息,也可以通過(guò)Windbg直接提供的命令去查看, 可以看到其分配空間的最小粒度(Granularity)為8字節(jié)。并且只有一個(gè)Segment.

  1. 0:006> !heap -a 00fa0000 
  2. Index   Address  Name      Debugging options enabled 
  3.   1:   00fa0000  
  4.     Segment at 00fa0000 to 0109f000 (00089000 bytes committed
  5.     Flags:                00000002 
  6.     ForceFlags:           00000000 
  7.     Granularity:          8 bytes 
  8.     Segment Reserve:      00100000 
  9.     Segment Commit:       00002000 
  10.     DeCommit Block Thres: 00000800 
  11.     DeCommit Total Thres: 00002000 
  12.     Total Free Size:      0000597f 
  13.     Max. Allocation Size: 7ffdefff 
  14.     Lock Variable at:     00fa0248 
  15.     Next TagIndex:        0000 
  16.     Maximum TagIndex:     0000 
  17.     Tag Entries:          00000000 
  18.     PsuedoTag Entries:    00000000 
  19.     Virtual Alloc List:   00fa009c 
  20.         03321000: 00100000 [commited 101000, unused 1000] - busy (b), tail fill 
  21.     Uncommitted ranges:   00fa008c 
  22.             01029000: 00076000  (483328 bytes) 
  23.     FreeList[ 00 ] at 00fa00c0: 00ffcf40 . 00ff3290   
  24.         00ff3288: 00208 . 00010 [100] - free 
  25.         00fb1370: 00060 . 00010 [100] - free 
  26.         00fb10a0: 00020 . 00010 [100] - free 
  27.         00fa6c40: 00088 . 00010 [100] - free 
  28.         00fa8e98: 00010 . 00010 [100] - free 
  29.         00fafa78: 000d0 . 00018 [100] - free 
  30.         00faea20: 00138 . 00018 [100] - free 
  31.         00fafc38: 00030 . 00020 [100] - free 
  32.         00ff4570: 00128 . 00028 [100] - free 
  33.         00faeeb8: 00058 . 00028 [100] - free 
  34.         00faf0c8: 00060 . 00028 [100] - free 
  35.         00fad980: 00050 . 00028 [100] - free 
  36.         00fb83f0: 00050 . 00040 [100] - free 
  37.         00faed78: 00030 . 00080 [100] - free 
  38.         00feebd8: 000e8 . 00080 [100] - free 
  39.         00faeb80: 00050 . 000d0 [100] - free 
  40.         00ff0398: 00148 . 000d8 [100] - free 
  41.         00fafed0: 000b0 . 000f0 [100] - free 
  42.         00fb8130: 00210 . 00270 [100] - free 
  43.         00fef460: 00808 . 003c8 [100] - free 
  44.         00ffcf38: 003c8 . 2c0a8 [100] - free 
  45.  
  46.     Segment00 at 00fa0000: 
  47.         Flags:           00000000 
  48.         Base:            00fa0000 
  49.         First Entry:     00fa0498 
  50.         Last Entry:      0109f000 
  51.         Total Pages:     000000ff 
  52.         Total UnCommit:  00000076 
  53.         Largest UnCommit:00000000 
  54.         UnCommitted Ranges: (1) 
  55.  
  56.     Heap entries for Segment00 in Heap 00fa0000 
  57.          address: psize . size  flags   state (requested size
  58.         00fa0000: 00000 . 00498 [101] - busy (497) 
  59.         00fa0498: 00498 . 00108 [101] - busy (100) 
  60.         00fa05a0: 00108 . 000d8 [101] - busy (d0) 
  61.  
  62.         ...... 
  63.         01029000:      00076000      - uncommitted bytes. 

查看Segment

一般來(lái)說(shuō)我們通過(guò)上述的命令已經(jīng)可以基本查看到Segment在一個(gè)堆中的信息了。如果要針對(duì)一個(gè)Segment進(jìn)行查看可以用如下方式:

  1. 0:006> dt _HEAP_SEGMENT 00fa0000 
  2. ntdll!_HEAP_SEGMENT 
  3.    +0x000 Entry            : _HEAP_ENTRY 
  4.    +0x008 SegmentSignature : 0xffeeffee 
  5.    +0x00c SegmentFlags     : 2 
  6.    +0x010 SegmentListEntry : _LIST_ENTRY [ 0xfa00a4 - 0xfa00a4 ] 
  7.    +0x018 Heap             : 0x00fa0000 _HEAP 
  8.    +0x01c BaseAddress      : 0x00fa0000 Void 
  9.    +0x020 NumberOfPages    : 0xff 
  10.    +0x024 FirstEntry       : 0x00fa0498 _HEAP_ENTRY 
  11.    +0x028 LastValidEntry   : 0x0109f000 _HEAP_ENTRY 
  12.    +0x02c NumberOfUnCommittedPages : 0x76 
  13.    +0x030 NumberOfUnCommittedRanges : 1 
  14.    +0x034 SegmentAllocatorBackTraceIndex : 0 
  15.    +0x036 Reserved         : 0 
  16.    +0x038 UCRSegmentList   : _LIST_ENTRY [ 0x1028ff0 - 0x1028ff0 ] 

查看申請(qǐng)的內(nèi)存地址

其實(shí)在調(diào)試過(guò)程中一般最關(guān)注的是變量的地址關(guān)聯(lián)的內(nèi)容信息。比如說(shuō)我寫(xiě)了個(gè)程序其申請(qǐng)的內(nèi)存變量地址為0x00fb5440, 申請(qǐng)的大小為5字節(jié)。

首先可以通過(guò)如下命令查找到地址所在的位置為堆:

  1. 0:000> !address 0x00fb5440 
  2.  
  3. Building memory map: 00000000 
  4. Mapping file section regions... 
  5. Mapping module regions... 
  6. Mapping PEB regions... 
  7. Mapping TEB and stack regions... 
  8. Mapping heap regions... 
  9. Mapping page heap regions... 
  10. Mapping other regions... 
  11. Mapping stack trace database regions... 
  12. Mapping activation context regions... 
  13.  
  14. Usage:                  Heap 
  15. Base Address:           00fa0000 
  16. End Address:            01029000 
  17. Region Size:            00089000 ( 548.000 kB) 
  18. State:                  00001000          MEM_COMMIT 
  19. Protect:                00000004          PAGE_READWRITE 
  20. Type:                   00020000          MEM_PRIVATE 
  21. Allocation Base:        00fa0000 
  22. Allocation Protect:     00000004          PAGE_READWRITE 
  23. More info:              heap owning the address: !heap 0xfa0000 
  24. More info:              heap segment 
  25. More info:              heap entry containing the address: !heap -x 0xfb5440 

然后可以通過(guò)如下命令查看當(dāng)前申請(qǐng)內(nèi)存的詳細(xì)堆塊信息, 其處于被占用狀態(tài)(busy)。可以看到其堆塊的大小為0x10, 我們實(shí)際申請(qǐng)的內(nèi)存為5字節(jié),那么0x10(Size) - 0xb (Unused) = 5, 可以看出來(lái)Unused是包含了_HEAP_ENTRY塊元數(shù)據(jù)的大小的。而我們實(shí)際用戶可用的內(nèi)存是8字節(jié) (最小分配粒度),比我們申請(qǐng)的5字節(jié)多了三個(gè)字節(jié),這也是為什么程序有時(shí)候溢出了幾個(gè)字符,并沒(méi)有導(dǎo)致程序崩潰或者異常的原因。

  1. 0:000> !heap -x 0xfb5440 
  2. Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags 
  3. ----------------------------------------------------------------------------- 
  4. 00fb5438  00fb5440  00fa0000  00fad348        10      -            b  LFH;busy 

那么我們也可以直接查看Entry的結(jié)構(gòu):

  1. 0:000> dt _HEAP_ENTRY 00fb5438 
  2. ntdll!_HEAP_ENTRY 
  3.    +0x000 UnpackedEntry    : _HEAP_UNPACKED_ENTRY 
  4.    +0x000 Size             : 0xa026 
  5.    +0x002 Flags            : 0xdc '' 
  6.    +0x003 SmallTagIndex    : 0x83 '' 
  7.    +0x000 SubSegmentCode   : 0x83dca026 
  8.    +0x004 PreviousSize     : 0x1b00 
  9.    +0x006 SegmentOffset    : 0 '' 
  10.    +0x006 LFHFlags         : 0 '' 
  11.    +0x007 UnusedBytes      : 0x8b '' 
  12.    +0x000 ExtendedEntry    : _HEAP_EXTENDED_ENTRY 
  13.    +0x000 FunctionIndex    : 0xa026 
  14.    +0x002 ContextValue     : 0x83dc 
  15.    +0x000 InterceptorValue : 0x83dca026 
  16.    +0x004 UnusedBytesLength : 0x1b00 
  17.    +0x006 EntryOffset      : 0 '' 
  18.    +0x007 ExtendedBlockSignature : 0x8b '' 
  19.    +0x000 Code1            : 0x83dca026 
  20.    +0x004 Code2            : 0x1b00 
  21.    +0x006 Code3            : 0 '' 
  22.    +0x007 Code4            : 0x8b '' 
  23.    +0x004 Code234          : 0x8b001b00 
  24.    +0x000 AgregateCode     : 0x8b001b00`83dca026 

如果細(xì)心的同學(xué)可以能會(huì)發(fā)現(xiàn)以下兩個(gè)問(wèn)題:

  1. 結(jié)構(gòu)中Size的值是0xa026和之前命令中看到的大小0x10不一樣,這個(gè)是因?yàn)閃indows對(duì)這些元數(shù)據(jù)做了編碼,需要用堆中的一個(gè)編碼數(shù)據(jù)做異或操作才能得到真實(shí)的值。具體方法筆者試過(guò),在這里不在贅述,可以在參考文章中獲取方法。
  2. Size是2字節(jié)描述,那么最大可以描述的大小應(yīng)該為0xffff,但是之前不是說(shuō)最大的塊可以是0x7FFF0 (524272字節(jié)), 應(yīng)該不夠存儲(chǔ)啊?這個(gè)也和第一個(gè)問(wèn)題有關(guān)聯(lián),在通過(guò)上述方法計(jì)算出的Size之后還需要乘以8, 才是真正的數(shù)據(jù)大小。

Windows 自建堆的使用建議

在<

保護(hù)組件

先看看書(shū)中原話:

假如你的應(yīng)用程序需要保護(hù)兩個(gè)組件,一個(gè)是節(jié)點(diǎn)結(jié)構(gòu)的鏈接表,一個(gè)是 B R A N C H結(jié)構(gòu)的二進(jìn)制樹(shù)。你有兩個(gè)源代碼文件,一個(gè)是 L n k L s t . c p p,它包含負(fù)責(zé)處理N O D E鏈接表的各個(gè)函數(shù),另一個(gè)文件是 B i n Tr e e . c p p,它包含負(fù)責(zé)處理分支的二進(jìn)制樹(shù)的各個(gè)函數(shù)。

現(xiàn)在假設(shè)鏈接表代碼中有一個(gè)錯(cuò)誤,它使節(jié)點(diǎn) 1后面的8個(gè)字節(jié)不

小心被改寫(xiě)了,從而導(dǎo)致分支 3中的數(shù)據(jù)被破壞。當(dāng)B i n Tr e e . c p p文件中的代碼后來(lái)試圖遍歷二進(jìn)制樹(shù)時(shí),它將無(wú)法進(jìn)行這項(xiàng)操作,因?yàn)樗膬?nèi)存已經(jīng)被破壞。當(dāng)然,這使你認(rèn)為二進(jìn)制樹(shù)代碼中存在一個(gè)錯(cuò)誤,而實(shí)際上錯(cuò)誤是在鏈接表代碼中。由于不同類(lèi)型的對(duì)象混合放在單個(gè)堆棧中,因此跟蹤和確定錯(cuò)誤將變得非常困難。

我個(gè)人認(rèn)為在一個(gè)應(yīng)用的工程中,也許不需要做到上述那么精細(xì)的劃分。但是你想一想,在一個(gè)大型工程中,會(huì)混合多個(gè)模塊。比如你是做產(chǎn)品的,那么產(chǎn)品會(huì)集成其他部門(mén)甚至是外部第三方的組件,那么這些組件同時(shí)在同一個(gè)進(jìn)程,使用同一個(gè)堆的時(shí)候,那么難免會(huì)出現(xiàn),A模塊的內(nèi)存溢出問(wèn)題,導(dǎo)致了B模塊的數(shù)據(jù)處理異常,從而讓你追蹤問(wèn)題異常復(fù)雜,更坑的是,很可能讓B模塊的團(tuán)隊(duì)背鍋了。而這些是切實(shí)存在的。 這里的建議更適合于讓一些關(guān)鍵模塊使用自己的堆,從而降低自己內(nèi)存使用不當(dāng),覆蓋了其他組件使用的內(nèi)存,從而導(dǎo)致異常,讓問(wèn)題的追蹤可以集中在出錯(cuò)的模塊中。當(dāng)然這也不是絕對(duì)的,因?yàn)檫M(jìn)程的組件都在同一個(gè)地址空間內(nèi),內(nèi)存破壞也存在一種跳躍式內(nèi)存訪問(wèn)破壞,但是大多數(shù)時(shí)候內(nèi)存溢出是連續(xù)的上溢較多,這樣做確實(shí)可以提高這種問(wèn)題追蹤的效率。

更有效的內(nèi)存管理

這個(gè)主要強(qiáng)調(diào)是,將同種類(lèi)型大小的對(duì)象放在一個(gè)堆中,盡量避免不同大小內(nèi)存對(duì)象摻雜在一起導(dǎo)致的內(nèi)存碎片問(wèn)題,從而帶來(lái)的堆管理效率下降。同一種對(duì)象,則可以避免內(nèi)存碎片問(wèn)題。當(dāng)然了這些只是提供了一種思想,至于你的工程是否有必要采用這樣的做法,由工程師自己來(lái)做決定。

進(jìn)行本地訪問(wèn)

先來(lái)看看原文的描述:

每當(dāng)系統(tǒng)必須在 R A M與系統(tǒng)的頁(yè)文件之間進(jìn)行 R A M頁(yè)面的交換時(shí),系統(tǒng)的運(yùn)行性能就會(huì)受到很大的影響。如果經(jīng)常訪問(wèn)局限于一個(gè)小范圍地址的內(nèi)存,那么系統(tǒng)就不太可能需要在 R A M與磁盤(pán)之間進(jìn)行頁(yè)面的交換。

所以,在設(shè)計(jì)應(yīng)用程序的時(shí)候,如果有些數(shù)據(jù)將被同時(shí)訪問(wèn),那么最好把它們分配在互相靠近的位置上。讓我們回到鏈接表和二進(jìn)制樹(shù)的例子上來(lái),遍歷鏈接表與遍歷二進(jìn)制樹(shù)之間并無(wú)什么關(guān)系。如果將所有的節(jié)點(diǎn)放在一起(放在一個(gè)堆棧中),就可以使這些節(jié)點(diǎn)位于相鄰的頁(yè)面上。實(shí)際上,若干個(gè)節(jié)點(diǎn)很可能恰好放入單個(gè)物理內(nèi)存頁(yè)面上。遍歷鏈接表將不需要 C P U為了訪問(wèn)每個(gè)節(jié)點(diǎn)而引用若干不同的內(nèi)存頁(yè)面。

這個(gè)思想其實(shí)就是一種Cache思想,RAM與磁盤(pán)上的page.sys存儲(chǔ)器(磁盤(pán)上的虛擬內(nèi)存)進(jìn)行頁(yè)交換會(huì)帶來(lái)一些時(shí)間成本。舉個(gè)極限的例子,你的RAM只有一個(gè)頁(yè),你有兩個(gè)對(duì)象A和B,A存放在Page1上,而B(niǎo)存放在Page2上,當(dāng)你訪問(wèn)A對(duì)象的時(shí)候,必然要把Page1的內(nèi)容加載到RAM中,那么這個(gè)時(shí)候B對(duì)象所在Page2肯定就在page.sys中,當(dāng)你又訪問(wèn)B對(duì)象的時(shí)候,這個(gè)時(shí)候就得把Page2從page.sys中加載到RAM中替換掉Page1.

理解了頁(yè)切換帶來(lái)的性能開(kāi)銷(xiāo)后,其實(shí)這一段的思想就是將最可能連續(xù)訪問(wèn)的對(duì)象放在一個(gè)堆中,那么他們?cè)谝粋€(gè)頁(yè)面的可能性也更大,提高了效率。

減少線程同步的開(kāi)銷(xiāo)

這一個(gè)很好理解,一般情況下創(chuàng)建的自建堆是支持多線程的,那么多線程的內(nèi)存分配必然會(huì)帶來(lái)同步的時(shí)間消耗,但是對(duì)于有些工程來(lái)說(shuō),只有一個(gè)線程,那么對(duì)于這一個(gè)線程的程序,在調(diào)用HeapCreate的時(shí)候設(shè)置HEAP_NO_SERIALIZE, 則這個(gè)堆只支持單線程,從而提高內(nèi)存申請(qǐng)的效率。

迅速釋放堆棧

這種思想第一提高了內(nèi)存釋放的效率,第二是盡可能的降低了內(nèi)存泄露。記得之前看過(guò)一篇文章介紹過(guò)Arena感覺(jué)比較類(lèi)似,在一個(gè)生命周期內(nèi)的內(nèi)存是從Arena申請(qǐng),然后這個(gè)聲明周期結(jié)束后,不是直接釋放各個(gè)對(duì)象,而是直接銷(xiāo)毀這個(gè)Arena,提高了釋放效率,并且降低了內(nèi)存泄露的可能。那么使用自建堆的原理和Arena是類(lèi)似的,比如在一個(gè)任務(wù)處理之前創(chuàng)建一個(gè)堆,在任務(wù)處理過(guò)程中所申請(qǐng)的內(nèi)存在這個(gè)堆上申請(qǐng),然后釋放的時(shí)候,直接銷(xiāo)毀這個(gè)堆即可。

那對(duì)于對(duì)象的申請(qǐng),C++中可以重載new和delete等操作符,來(lái)實(shí)現(xiàn)自定義的內(nèi)存分配,并且可以將這個(gè)先封裝成一個(gè)基類(lèi),在這個(gè)過(guò)程中需要?jiǎng)?chuàng)建的對(duì)象均繼承于這個(gè)基類(lèi),復(fù)用new和delete。

總結(jié)和參考

我本以為這些是已經(jīng)掌握的知識(shí),但是寫(xiě)文章的時(shí)間也超過(guò)了我預(yù)想的時(shí)間,在實(shí)踐中也也發(fā)現(xiàn)了一些自己曾經(jīng)錯(cuò)誤的理解。如果文中還有不當(dāng)?shù)牡胤剑蚕Mx者給與指正。

參考

《Windows核心編程》

《Windows高級(jí)調(diào)試》

Windows Heap Chunk Header Parsing and Size Calculation: https://stackoverflow.com/questions/28483473/windows-heap-chunk-header-parsing-and-size-calculation

Understanding the Low Fragmentation Heap: http://www.illmatics.com/Understanding_the_LFH.pdf

 

WINDOWS 10SEGMENT HEAP INTERNALS: https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf

 

責(zé)任編輯:武曉燕 來(lái)源: 一個(gè)程序員的修煉之路
相關(guān)推薦

2022-07-04 10:51:27

數(shù)據(jù)中臺(tái)數(shù)據(jù)倉(cāng)庫(kù)

2021-02-19 09:19:11

消息隊(duì)列場(chǎng)景

2018-08-21 14:42:29

閃存存在問(wèn)題

2022-02-14 22:22:30

單元測(cè)試Junit5

2014-07-17 10:11:53

Android LAPI谷歌

2018-01-11 09:51:34

2021-05-11 08:48:23

React Hooks前端

2021-11-23 09:45:26

架構(gòu)系統(tǒng)技術(shù)

2015-03-27 15:07:55

云計(jì)算IaaS平臺(tái)Docker

2016-07-08 13:33:12

云計(jì)算

2017-11-21 14:32:05

容器持久存儲(chǔ)

2016-10-09 23:47:04

2021-03-15 22:42:25

NameNodeDataNode分布式

2011-07-28 09:22:56

Oracle WDPOracle數(shù)據(jù)庫(kù)

2020-04-08 10:18:56

MySQL數(shù)據(jù)庫(kù)SQL

2019-01-30 10:59:48

IPv6Happy EyebaIPv4

2018-08-28 06:42:06

邊緣計(jì)算SDNMEC

2020-06-19 15:32:56

HashMap面試代碼

2019-11-12 08:40:03

RocketMQ架構(gòu)

2020-11-20 10:22:34

代碼規(guī)范設(shè)計(jì)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 日本一区二区高清不卡 | 黄片毛片免费观看 | 欧美日韩精品中文字幕 | 人人操日日干 | 婷婷中文字幕 | 91麻豆精品国产91久久久更新资源速度超快 | 黄色一级毛片 | 美女逼网站 | av天天看| 最新中文字幕在线播放 | 欧美日韩国产一区二区三区不卡 | 亚洲精品视频在线播放 | 精品二区 | 国产精品毛片 | 黄网站免费在线看 | 久久精品二区亚洲w码 | 成人免费在线视频 | 久久人操 | avav在线看 | 黄色一级电影免费观看 | 国产乱码精品一区二区三区中文 | 伊人影院99| 91精品国产综合久久久动漫日韩 | 欧美日韩网站 | 99热首页| 欧美日韩亚洲一区 | 国产91av视频 | 日本综合在线观看 | 91网站在线播放 | 欧美在线一区二区三区 | 九九热九九 | 九九热精品在线 | 亚洲免费视频在线观看 | 亚洲成人免费av | 日本不卡高清视频 | 久久视频精品 | 亚洲精品第一 | 国内精品视频一区二区三区 | 国产超碰人人爽人人做人人爱 | 欧美精品一区二区三区蜜臀 | 成人精品一区 |