內存管理

對內存資源分配和使用的技術

內存管理,是指軟體運行時對計算機內存資源的分配和使用的技術。其最主要的目的是如何高效,快速的分配,並且在適當的時候釋放和回收內存資源。一個執行中的程式,譬如網頁瀏覽器在個人電腦或是圖靈機(Turing machine)裡面,為一個行程將資料轉換於真實世界及電腦內存之間,然後將資料存於電腦內存內部(在計算機科學,一個程式是一群指令的集合,一個行程是電腦在執行中的程式)。一個程式結構由以下兩部分而成:“本文區段”,也就是指令存放,提供CPU使用及執行; “資料區段”,儲存程式內部本身設定的資料,例如常數字串。

技術簡介


內存可以通過許多媒介實現,例如磁帶或是 磁碟,或是小陣列容量的微晶元。從1950年代開始,計算機變的更複雜,它內部由許多種類的內存組成。內存管理的任務也變的更加複雜,甚至必須在一台機器同時執行多個進程。
虛擬內存是內存管理技術的一個極其實用的創新。它是一段程序(由 操作系統調度),持續監控著所有 物理內存中的 代碼段、數據段,並保證他們在運行中的效率以及可靠性,對於每個 用戶層(user-level)的進程分配一段 虛擬內存空間。當進程建立時,不需要在 物理內存件之間搬移數據,數據儲存於 磁碟內的 虛擬內存空間,也不需要為該進程去配置主內存空間,只有當該進程被被調用的時候才會被載入到主內存。
可以想像一個很大的程序,當他執行時被 操作系統調用,其運行需要的內存數據都被存到 磁碟內的 虛擬內存,只有需要用到的部分才被載入到主內存內部運行。

主內存


當一個程序執行,操作系統將程序的資料區段及本文區段映射到 虛擬內存空間內部,然後在內存執行程序的指令(見 馮諾依曼架構(von Neumann architecture),無論如何,當進程執行時就必須去儲存暫時性的資料,或更重要的,它會呼叫一些函數(function)或是子程序(subroutine),並且儲存當前函數的狀態,最好的 數據結構方法,資料由 堆棧(stack)的方式儲存,當我們完成這個函數,資料會由堆棧的pop方式取出,堆棧將會在函數的生命周期內動態的成長,操作系統提供區分本文區段及資料區段,而堆棧區段則在一個行程的最頂端,這種方式稱為段式結構(segments)或“分段”。

內存管理


內存管理對於編寫出高效率的Windows程序是非常重要的,這是因為Windows是多任務系統,它的內存管理和單任務的DOS相比有很大的差異。DOS是單任務 操作系統,應用程序分配到內存后,如果它不主動釋放,系統是不會對它作任何改變的;但Windows卻不然,它在同一時刻可能有多個應用程序 共享內存,有時為了使某個任務更好地執行,Windows系統可能會對其它任務分配的內存進行移動,甚至刪除。因此,我們在Windows應用程序中使用內存時,要遵循Windows內存管理的一些約定,以盡量提高Windows內存的利用率。

內存對象


Windows應用程序可以申請分配屬於自己的內存塊,內存塊是應用程序操作內存的單位,它也稱作內存對象,在Windows中通過內存句柄來操作內存對象。內存對象根據分配的範圍可分為全局內存對象和局部內存對象;根據性質可分為固定內存對象,可移動內存對象和可刪除內存對象。
固定內存對象,特別是局部固定內存對象和DOS的內存塊很類似,它一旦分配,就不會被移動或刪除,除非應用程序主動釋放它。並且對於局部固定內存對象來說,它的內存句柄本身就是內存對象的16位近地址,可供應用程序直接存取,而不必象其它類型的內存對象那樣要通過鎖定在內存某固定地址后才能使用。
可移動內存對象沒有固定的地址,Windows系統可以隨時把它們移到一個新地址。內存對象的可移動使得Windows能有效地利用自由內存。例如,如果一個可移動的內存對象分開了兩個自由內存對象,Windows可以把可移動內存對象移走,將兩個自由內存對象合併為一個大的自由內存對象,實現內存的合併與碎片回收。
可刪除內存對象與可移動內存對象很相似,它可以被Windows移動,並且當Windows需要大的內存空間滿足新的任務時,它可以將可刪除內存對象的長度置為0,丟棄內存對象中的數據。
可移動內存對象和可刪除內存對象在存取前必須使用內存加鎖函數將其鎖定,鎖定了的內存對象不能被移動和刪除。因此,應用程序在使用完內存對象后要儘可能快地為內存對象解鎖。內存需要加鎖和解鎖增加了程序員的負擔,但是它卻極大地改善了Windows內存利用的效率,因此Windows鼓勵使用可移動和可刪除的內存對象,並且要求應用程序在非必要時不要使用固定內存對象。
不同類型的對象在它所處的內存堆中的位置是不一樣的,圖6.2說明內存對象在堆中的位置:固定對象位於堆的底部;可移動對象位於固定對象之上;可刪除對象從堆的頂部開始分配。

局部內存

局部內存對象在局部堆中分配,局部堆是應用程序獨享的自由內存,它只能由應用程序的特定實例訪問。局部堆建立在應用程序的 數據段中,因此,用戶可分配的局部內存對象的最大內存空間不能超過64K。局部堆由Windows應用程序在 模塊定義文件中用HEAPSIZE語句申請,HEAPSIZE指定以 位元組為單位的局部堆初始空間尺寸。Windows提供了一系列函數來操作局部內存對象。
分配局部內存對象
LocalAlloc函數用來分配局部內存,它在應用程序局部堆中分配一個內存塊,並返回內存塊的句柄。LocalAlloc函數可以指定內存對象的大小和特性,其中主要特性有固定的(LMEM_FIXED),可移動的(LMEM_MOVEABLE)和可刪除的(LMEM_DISCARDABLE)。如果局部堆中無法分配申請的內存,則LocalAlloc函數返回NULL。下面的代碼用來分配一個固定內存對象,因為局部固定內存對象的對象句柄其本身就是16位內存近地址,因此它可以被應用程序直接存取。
加鎖與解鎖
上面 程序段分配的固定局部內存對象可以由應用程序直接存取,但是,Windows並不鼓勵使用固定內存對象。因此,在使用可移動和可刪除內存對象時,就要經常用到對內存對象的加鎖與解鎖。
不管是可移動對象還是可刪除對象,在它分配后其內存句柄是不變的,它是內存對象的恆定引用。但是,應用程序無法通過內存句柄直接存取內存對象,應用程序要存取內存對象還必須獲得它的近地址,這通過調用 LocalLock函數實現。 LocalLock函數將局部內存對象暫時固定在局部堆的某一位置,並返回該地址的近地址值,此地址可供應用程序存取內存對象使用,它在應用程序調用 LocalUnlock函數解鎖此內存對象之前有效。
應用程序在使用完內存對象后,要儘可能早地為它解鎖,這是因為Windows無法移動被鎖住了的內存對象。當應用程序要分配其它內存時,Windows不能利用被鎖住對象的區域,只能在它周圍尋找,這會降低Windows內存管理的效率。
改變局部內存對象
局部內存對象分配之後,還可以調用LocalReAlloc函數進行修改。LocalReAlloc函數可以改變局部內存對象的大小而不破壞其內容:如果比原來的空間小,則Windows將對象截斷;如果比原來大,則Windows將增加區域填0(使用LMEM_ZEROINIT選項),或者不定義該區域內容。另外,LocalReAlloc函數還可以改變對象的屬性,如將屬性從LMEM_MOVEABLE改為LMEM_DISCARDABLE,或反過來,此時必須同時指定LMEM_MODIFY選項。但是,LocalReAlloc函數不能同時改變內存對象的大小和屬性,也不能改變具有LMEM_FIXED屬性的內存對象和把其它屬性的內存對象改為LMEM_FIXED屬性。
釋放與刪除
分配了的局部內存對象可以使用LocalDiscard和LocalFree函數來刪除和釋放,刪除和釋放只有在內存對象未鎖住時才有效。
LocalFree函數用來釋放局部內存對象,當一個局部內存對象被釋放時,其內容從局部堆移走,並且其句柄也從有效的局部內存表中移走,原來的內存句柄變為不可用。LocalDiscard 函數用來刪除局部內存對象,它只移走對象的內容,而保持其句柄有效,用戶在需要時,還可以使用此內存句柄用LocalReAlloc函數重新分配一塊內存。
另外,Windows還提供了函數 LocalSize用於檢測對象所佔空間;函數LocalFlags用於檢測內存對象是否可刪除,是否已刪除,及其鎖計數值;函數LocalCompact用於確定局部堆的可用內存。

全局內存

全局內存對象在全局堆中分配,全局堆包括所有的系統內存。一般來說,應用程序在全局堆中進行大型 內存分配(約大於1KB),在全局堆還可以分配大於64K的巨型內存,這將在後面介紹。
分配全局內存對象
全局內存對象使用GlobalAlloc函數分配,它和使用LocalAlloc分配局部內存對象很相似。使用GlobalAlloc的例子我們將和GlobalLock一起給出。
加鎖與解鎖
全局內存對象使用GlobalLock函數加鎖,所有全局內存對象在存取前都必須加鎖。GlobalLock將對象鎖定在內存固定位置,並返回一個 遠指針,此指針在調用GlobalUnlock之前保持有效。
GlobalLock和 LocalLock稍有不同,因為全局內存對象可能被多個任務使用,因此在使用GlobalLock加鎖某全局內存對象時,對象可能已被鎖住,為了處理這種情況,Windows增加了一個鎖計數器。當使用GlobalLock加鎖全局內存對象時,鎖計數器加1;使用GlobalUnlock解鎖對象時,鎖計數器減1,只有當鎖計數器為0時,Windows才真正解鎖此對象。
修改全局內存對象
修改全局內存對象使用 GlobalReAlloc函數,它和LocalReAlloc函數很類似,這裡不再贅述。修改全局內存對象的特殊之處在於巨型對象的修改上,這一點我們將在後面講述。
內存釋放及其它操作
全局內存對象使用GlobalFree函數和GlobalDiscard來釋放與刪除,其作用與LocalFree和LocalDiscard類似。GlobalSize函數可以檢測內存對象大小;GlobalFlags函數用來檢索對象是否可刪除,是否已刪除等信息;GlobalCompact函數可以檢測全局堆可用內存大小。
巨型內存對象
如果全局內存對象的大小為64KB或更大,那它就是一個巨型內存對象,使用GlobalLock函數加鎖巨型內存對象將返回一個巨型 指針。
巨型內存對象的修改有一點特殊性,當對象大小增加並超過64K的倍數時,Windows可能要為重新分配的內存對象返回一個新的全局句柄,

段介紹

Windows採用段的概念來管理應用程序的內存,段有 代碼段和 數據段兩種,一個應用程序可有多個 代碼段和數據段。代碼段和 數據段的數量決定了應用程序的內存模式,圖6.2說明了內存模式與應用程序代碼段和數據段的關係。
段的管理和全局內存對象的管理很類似,段可以是固定的,可移動的和可刪除的,其屬性在應用程序的 模塊定義文件中指定。段在全局內存中分配空間,Windows鼓勵使用可移動的代碼段和 數據段,這樣可以提高其內存利用效率。使用可刪除的 代碼段可以進一步減小應用程序對內存的影響,如果代碼段是可刪除的,在必要時Windows將其刪除以滿足對全局內存的請求。被刪除的段由Windows監控,當應用程序利用該 代碼段時,Windows自動地將它們重新裝入。
代碼段
代碼段是不超過64K位元組的 機器指令,它代表全部或部分應用程序指令。代碼段中的數據是只讀的,對代碼段執行寫操作將引起通用保護(GP)錯誤。
每個應用程序都至少有一個 代碼段,例如我們前面幾章的例子都只有一個代碼段。用戶也可以生成有多個 代碼段的應用。實際上,多數Windows應用程序都有多個 代碼段。通過使用多 代碼段,用戶可以把任何給定代碼段的大小減少到完成某些任務所必須的幾條指令。這樣,可通過使某些段可刪除,來優化應用程序對內存的使用。
中模式和大模式的應用程序都使用多 代碼段,這些應用程序的每一個段都有一個或幾個 源文件。對於多個 源文件,將它們分開各自編譯,為編譯過的代碼所屬的每個段命名,然後連接。段的屬性在 模塊定義文件中定義,Windows使用SEGMENTS語句來完成此任務,如下面的代碼定義了四個段的屬性:
用戶也可以在 模塊定義文件中用CODE語句為所有未顯式定義過的 代碼段定義預設屬性。例如,要將未列在SEGMENTS語句中的所有段定義為可刪除的,可用下面的語句:
CODE MOVEABLE DISCARDABLE。
數據段
每個應用程序都有一個數據段,數據段包含應用程序的 堆棧、局部堆、靜態數據和全局數據。一個數據段的長度也不能超過64K。數據段可以是固定的或可移動的,但不能是可刪除的。如果 數據段是可移動的,Windows在將控制轉嚮應用程序前自動為其加鎖,當應用程序分配全局內存,或試圖在局部堆中分配超過當前可分的內存時,可移動數據段可能被移動,因此在數據段中不要保留指向變數的長 指針,當數據段移動時,此長指針將失效。
在 模塊定義文件中用DATA語句定義 數據段的屬性,屬性的 預設值為MOVEABLE和MULTIPLE。MULTIPLE屬性使Windows為應用程序的每一個實例拷貝一個應用程序數據段,這就是說每個應用程序實例中數據段的內容都是不同的。
內存管理程序示例Memory
應用程序Memory示例了部分內存管理,它是一個使用了可刪除 代碼段的中模式Windows應用程序。Memory程序有四個C語言 源程序,在 模塊定義文件中顯示定義了四個 代碼段,相應地模塊定義文件和makefile文件有地些修改,讀者可通過比較Memory程序和5.1.2節的例子來體會它們之間的不同。另外,讀者在編譯和連接應用程序Memory后,可用Visual C++提供的Windows Heap Walker (HEAPWALK.EXE)來觀察Memory運行時的各個段。

動態連接庫

使用動態連接庫是Windows的一個很重要的特點,它使得多個Windows應用程序可以共享函數代碼、數據和硬體,這可以大大提高Windows內存的利用率。
動態連接庫是一個可執行模塊,它包含的函數可以由Windows應用程序調用執行,為應用程序提供服務。它和我們以前用的C函數庫相比,在功能上是很類似的,其主要區別是 動態連接庫在運行是連接,C函數庫(靜態連接庫)是在生成 可執行文件時由連接器(LINK)連接。靜態連接庫中的代碼在應用程序生成以後已經連接到應用程序模塊之中,但 動態連接庫中的代碼只有在應用程序要用到該代碼段時才動態調入DLL中的相應代碼。為了讓應用程序在執行時能夠調入DLL中正確的代碼,Windows提供了 動態連接庫的引入庫。Windows在連接生成應用程序時,如果使用 動態連接庫函數,連接器並不拷貝DLL中的任何代碼,它只是將引入庫中指定所需函數在DLL中位置的信息拷貝在應用程序模塊中,當應用程序運行時,這些定位信息在可執行應用程序和動態連接庫之間建立動態連接。靜態庫、引入庫和動態庫之間的區別如表6.1所示。
DLL不能獨立執行,也不能使用 消息循環。每個DLL都有一個 入口點和一個出口點,具有自己的實例句柄、數據段和局部堆,但DLL沒有 堆棧,它使用調用程序的堆棧。DLL也包括有.C文件,.H文件,.RC文件和.DEF文件,另外,在連接時一般要加入SDK庫中的LIBENTRY.OBJ文件。
創建動態連接庫
要創建 動態連接庫,至少有三個文件:
C語言 源文件;
一個 模塊定義文件(.DEF);
makefile文件。
有了這些文件后,就可以運行Microsoft的程序維護機制(NMAKE),編譯並連接 源代碼文件,生成DLL文件。
創建C語言源文件
和其它C應用程序一樣,動態連接庫可包含多個函數,每個函數要在被其它應用程序或庫使用之前用FAR聲明,並且在庫的 模塊定義文件中用EXPORTS語句引出。
在上面的 源代碼中,有兩個函數是DLL源代碼所必需的,這就是DLL入口函數LibMain和出口函數WEP。
LibMain函數是DLL的 入口點,它由DLL 自動初始化函數LibEntry調用,主要用來完成一些初始化任務。LibMain有四個參數:hint, wDataSeg, cbHeapSize和lpszCmdLine。其中hInst是 動態連接庫的實例句柄;wDataSeg是 數據段(DS)寄存器的值;cbHeapSize是 模塊定義文件定義的堆的尺寸,LibEntry函數用該值來初始化局部堆;lpszCmdLine包含命令行的信息。
WEP函數是DLL的標準出口函數,它在DLL被卸出之前由Windows調用執行,以完成一些必要的清除工作。WEP函數只使用一個參數nParameter,它用來指示終止狀態。
源文件中的其它函數則是DLL為應用程序提供的 庫函數,DLL設計者可以給它加入自己所需要的功能,如DrawBox,DrawPie和DrawCircle。
建立DLL模塊定義文件
每個DLL必須有一個 模塊定義文件,該文件在使用LINK連接時用於提供定義庫屬性的引入信息。
關鍵字LIBRARY用來標識這個模塊是一個 動態連接庫,其後是庫名DRAWDLL,它必須和動態連接庫文件名相同。
DATA語句中關鍵字SINGLE是必須的,它表明無論應用程序訪問DLL多少次,DLL均只有單個 數據段。
其它關鍵字的用法同Windows應用程序的 模塊定義文件一樣,這在前面已有敘述,請參見5.1.2.3。
編製Makefile文 件
NMAKE是Microsoft的程序維護機制,它控制執行文件的創建工作,以保證只有必要的操作被執行。有五種工具用來創建 動態連接庫:
CL
Microsoft C優化 編譯器,它將C語言源文件編譯成目標文件.OBJ。
LINK
Microsoft 分段可執行連接器,它將目標文件和 靜態庫連接生成 動態連接庫。LINK命令行有五個參數,用逗號分開:第一個參數列出所有 動態連接庫用到的目標文件(.OBJ),如果使用了標準動態連接初始化函數,則必須包括LIBENTRY.OBJ文件;第二個參數指示最終可執行文件名,一般用.DLL作為擴展名;第三個參數列出創建動態連接庫所需要的引入庫和 靜態庫;第五個參數是 模塊定義文件。
IMPLIB
Microsoft引入庫管理器,它根據 動態連接庫的 模塊定義文件創建一個擴展名為.LIB的引入庫。
RC
Microsoft Windows資源 編譯器。所有 動態連接庫都必須用RC編譯,以使它們與Windows 3.1版兼容。
MAPSYM
Microsoft 符號文件生成器,它是可選工具,只用於調試版本。

程序訪問

應用程序要訪問 動態連接庫函數,它應該做下面三件事:建立庫函數原型,調用庫函數,引入庫函數。建立 庫函數原型一般通過在C語言 源文件中包含動態連接庫的頭文件解決,
頭文件中包含了每個 庫函數的原型語句,原型語句的目的是為 編譯器定義函數的參數和返回值,以使編譯器能正確創建調用庫函數的代碼。原型語句定義好之後,應用程序就可以象調用靜態連接 庫函數一樣調用 動態連接庫的函數了。
應用程序調用DLL中的 引出函數還要在應用程序中對其進行引入,一般有三種方法:
連接時隱式引入
最常用也最簡單的方法是連接時隱式引入,這種方法是在應用程序的連接命令行中列出為 動態連接庫創建的引入庫,這樣應用程序在使用DLL的 引出函數時,就如同使用 靜態庫中的函數一樣了。
連接時顯式引入
和隱式引入一樣,顯式引入也是在連接時進行的,它通過把所需函數列在應用程序的 模塊定義文件的IMPORTS語句中完成。對於在 模塊定義文件中定義了入口序號的DLL函數,採用引入函數名、動態連接庫名和入口序號的形式,如:
IMPORTS
DrawBox=DllDraw.2
如果DLL的 模塊定義文件沒有定義 引出函數的入口序號,則使用如下引入語句:
IMPORTS
DllDraw.DrawBox
運行時動態引入
應用程序可以在運行時動態連接DLL函數,當需要調用DLL的 引出函數時,應用程序首先裝入庫,並直接檢索所需函數地址,然後才調用該函數。