Windows核心编程 第十八章 堆栈
第1?8章?堆?棧
????對內存進行操作的第三個機制是使用堆棧。堆棧可以用來分配許多較小的數據塊。例如,若要對鏈接表和鏈接樹進行管理,最好的方法是使用堆棧,而不是第?1?5章介紹的虛擬內存操作方法或第1?7章介紹的內存映射文件操作方法。堆棧的優點是,可以不考慮分配粒度和頁面邊界之類的問題,集中精力處理手頭的任務。堆棧的缺點是,分配和釋放內存塊的速度比其他機制要慢,并且無法直接控制物理存儲器的提交和回收。
從內部來講,堆棧是保留的地址空間的一個區域。開始時,保留區域中的大多數頁面沒有被提交物理存儲器。當從堆棧中進行越來越多的內存分配時,堆棧管理器將把更多的物理存儲器提交給堆棧。物理存儲器總是從系統的頁文件中分配的,當釋放堆棧中的內存塊時,堆棧管理器將收回這些物理存儲器。
18.1?進程的默認堆棧
????當進程初始化時,系統在進程的地址空間中創建一個堆棧。該堆棧稱為進程的默認堆棧。
按照默認設置,該堆棧的地址空間區域的大小是?1?MB。但是,系統可以擴大進程的默認堆棧,使它大于其默認值。當創建應用程序時,可以使用?/?H?E?A?P鏈接開關,改變堆棧的1?M?B默認區域大小。由于?D?L?L沒有與其相關的堆棧,所以當鏈接?D?L?L時,不應該使用?/?H?E?A?P鏈接開關。
/?H?E?A?P鏈接開關的句法如下:
/?H?E?A?P:reserve[,commit]
許多Wi?n?d?o?w?s函數要求進程使用其默認堆棧。例如,?Windows?2000的核心函數均使用U?n?i?c?o?d?e字符和字符串執行它們的全部操作。如果調用Wi?n?d?o?w?s函數的A?N?S?I版本,那么該A?N?S?I版本必須將A?N?S?I字符串轉換成U?n?i?c?o?d?e字符串,然后調用同一個函數的?U?n?i?c?o?d?e版本。為了進行字符串的轉換,A?N?S?I函數必須分配一個內存塊,以便放置U?n?i?c?o?d?e版本的字符串。該內存塊是從你的進程的默認堆棧中分配的。?Wi?n?d?o?w?s的其他許多函數需要使用一些臨時內存塊,這些內存塊是從進程的默認堆棧中分配的。
由于進程的默認堆棧可供許多Wi?n?d?o?w?s函數使用,你的應用程序有許多線程同時調用各種Wi?n?d?o?w?s函數,因此對默認堆棧的訪問是順序進行的。
單個進程可以同時擁有若干個堆棧。這些堆棧可以在進程的壽命期中創建和撤消。但是,
默認堆棧是在進程開始執行之前創建的,并且在進程終止運行時自動被撤消。不能撤消進程的默認堆棧。每個堆棧均用它自己的堆棧句柄來標識,用于分配和釋放堆棧中的內存塊的所有堆棧函數都需要這個堆棧句柄作為其參數。
可以通過調用G?e?t?P?r?o?c?e?s?s?H?e?a?p函數獲取你的進程默認堆棧的句柄:
HANDLE?GetProcessHeap();
18.2?為什么要創建輔助堆棧
除了進程的默認堆棧外,可以在進程的地址空間中創建一些輔助堆棧。由于下列原因,你可能想要在自己的應用程序中創建一些輔助堆棧:
??保護組件。
??更加有效地進行內存管理。
??進行本地訪問。
??減少線程同步的開銷。
??迅速釋放。
下面讓我們來詳細說明每個原因。
18.2.1?保護組件
????假如你的應用程序需要保護兩個組件,一個是節點結構的鏈接表,一個是?B?R?A?N?C?H結構的二進制樹。你有兩個源代碼文件,一個是?L?n?k?L?s?t?.?c?p?p,它包含負責處理
N?O?D?E鏈接表的各個函數,另一個文件是B?i?n?Tr?e?e?.?c?p?p,它包含負責處理
分支的二進制樹的各個函數。
????如果節點和分支一道存儲在單個堆棧中,那么這個組合堆棧將類似圖1?8?-?1所示的樣子。
????現在假設鏈接表代碼中有一個錯誤,它使節點?1后面的8個字節不小心被改寫了,從而導致分支?3中的數據被破壞。當B?i?n?Tr?e?e?.?c?p?p文件中的代碼后來試圖遍歷二進制樹時,它將無法進行這項操作,因為它的內存已經被破壞。當然,這使你認為二進制樹代碼中存在一個錯誤,而實際上錯誤是在鏈接表代碼中。由于不同類型的對象混合放在單個堆棧中,因此跟蹤和確定錯誤將變得非常困難。
????通過創建兩個獨立的堆棧,一個堆棧用于存放節點,另一個堆棧用于存放分支,就能夠確定你的問題。你的鏈接表代碼中的一個小錯誤不會破壞你的二進制樹的完整性。反過來,二進制樹中的小錯誤也不會影響鏈接表代碼中的數據完整性。但是,你的代碼中的錯誤仍然
可能導致對堆棧進行雜亂的內存寫操作,不過出現這種情況的可能性很小。
18.2.2?更有效的內存管理
????通過在堆棧中分配同樣大小的對象,就可以更加有效地管理堆棧。例如,假設每個節點結構需要2?4字節,每個分支結構需要3?2字節。所有這些對象均從單個堆棧中分配。圖?1?8?-?2顯示了單個堆棧中已經分配的若干個節點和分支對象占滿了這個堆棧。如果節點?2和節點4被釋放,堆棧中的內存將變成許多碎片。這時,如果試圖分配分支結構,那么盡管分支只需要3?2個字節,而實際上可以使用的有4?8個字節,但是分配仍將失敗。
如果每個堆棧只包含大小相同的對象,那么釋放一個對象后,另一個對象就可以恰好放入被釋放的對象空間中。
18.2.3?進行本地訪問
????每當系統必須在R?A?M與系統的頁文件之間進行?R?A?M頁面的交換時,系統的運行性能就會受到很大的影響。如果經常訪問局限于一個小范圍地址的內存,那么系統就不太可能需要在?R?A?M與磁盤之間進行頁面的交換。
????所以,在設計應用程序的時候,如果有些數據將被同時訪問,那么最好把它們分配在互相靠近的位置上。讓我們回到鏈接表和二進制樹的例子上來,遍歷鏈接表與遍歷二進制樹之間并無什么關系。如果將所有的節點放在一起(放在一個堆棧中),就可以使這些節點位于相鄰的頁面上。實際上,若干個節點很可能恰好放入單個物理內存頁面上。遍歷鏈接表將不需要?C?P?U為了訪問每個節點而引用若干不同的內存頁面。
如果將節點和分支分配在單個頁面上,那么節點就不一定會互相靠在一起。在最壞的情況下,每個內存頁面上可能只有一個節點,而其余的每個頁面則由分支占用。在這種情況下,遍歷鏈接表將可能導致每個節點的頁面出錯,從而使進程運行得極慢。
18.2.4?減少線程同步的開銷
????正如下面就要介紹的那樣,按照默認設置,堆棧是順序運行的,這樣,如果多個線程試圖同時訪問堆棧,就不會使數據受到破壞。但是,堆棧函數必須執行額外的代碼,以保證堆棧對線程的安全性。如果要進行大量的堆棧分配操作,那么執行這些額外的代碼會增加很大的負擔,從而降低你的應用程序的運行性能。當你創建一個新堆棧時,可以告訴系統,只有一個線程將訪問該堆棧,因此額外的代碼將不執行。但是要注意,現在你要負責保證堆棧對線程的安全性。系統將不對此負責。
18.2.5?迅速釋放堆棧
????最后要說明的是,將專用堆棧用于某些數據結構后,就可以釋放整個堆棧,而不必顯式釋放堆棧中的每個內存塊。例如,當Windows?Explorer遍歷硬盤驅動器的目錄層次結構時,它必須在內存中建立一個樹狀結構。如果你告訴?Windows?Explorer刷新它的顯示器,它只需要撤消包含這個樹狀結構的堆棧并且重新運行即可(當然,假定它將專用堆棧用于存放目錄樹信息)。對于許多應用程序來說,這是非常方便的,并且它們也能更快地運行。
?
之后的內容就是介紹一些函數,這里我就把函數寫出來,具體使用細節可以查看文檔。
1.創建輔助堆棧 HANDLE HeapCreate( DWORD fdwOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize); 2.從堆棧中分配內存塊 PVOID HeapAlloc( HANDLE hHeap, DWORD fdwFlags, SIZE_T dwButes); 3.改變內存塊大小 PVOID HeapReAlloc( HANDLE hHeap, DWORD fdwFlags, PVOID pvMem, SIZE_T dwBytes); 4.了解內存塊大小 SIZE_T HeapSize( HANDLE hHeap, DWORD fdwFlags, LPCVOID pvMem); 5.釋放內存塊 BOOL HeapFree( HANDLE hHeap, DWORD fdwFlags, PVOID pvMem); 6.撤銷堆棧 BOOL HeapDestroy(HANDLE hHeap); 7.獲取現有堆棧信息 DWPRD GetProcessHeaps( DWORD dwNumHeaps, PNANDLE pHeaps); 8.驗證堆棧完整性 BOOL HeapVa;odate( HANDLE hHeap, DWORD fdwFlags, LPCVOID pvMem); 9.合并地址中的空閑內存塊回收不包含已經分配的地址內存塊的存儲器頁面 UINT HeapCompact( HANDLE hHeap, DWORD fdwFlags);10.堆棧上鎖 BOOL HeapLock(HANDLE hHeap); BOOL HeapUnLock(HANDLE hHeap); 11.遍歷堆棧內容 BOOL HeapWalk( HANDLE hHeap, PPROCESS_HEAP_ENTRY pHeapEntry);
總結
以上是生活随笔為你收集整理的Windows核心编程 第十八章 堆栈的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PowerShell-6.文件操作
- 下一篇: Windows核心编程 第十九章 DLL