技術簡介
記憶體可以通過許多媒介實現,例如磁帶或是磁碟,或是小陣列容量的微晶片。 從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鼓勵使用可移動和可刪除的記憶體對象,並且要求應用程式在非必要時不要使用固定記憶體對象。
不同類型的對象在它所處的記憶體堆中的位置是不一樣的,定對象位於堆的底部;可移動對象位於固定對象之上;可刪除對象從堆的頂部開始分配。
局部記憶體
局部記憶體對象在局部堆中分配,局部堆是應用程式獨享的自由記憶體,它只能由應用程式的特定實例訪問。局部堆建立在應用程式的數據段中,因此,用戶可分配的局部記憶體對象的最大記憶體空間不能超過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的引出函式時,應用程式首先裝入庫,並直接檢索所需函式地址,然後才調用該函式。