記憶體管理 如果你在寫Windows CE 程式中遇到的最重要的問題,那一定是記憶體問題。一個WinCE 系統可能只有4MB 的RAM,這相對於個人電腦來說是十分少的,因為個人電腦的標準配置已經到了128MB 甚至更多。事實上,運行WinCE 的機器的記憶體十分缺乏,以至於有時候有必要在寫程式的時候為節約記憶體而犧牲程式的整體效能。 幸運的是,儘管WinCE系統的記憶體很小,但可用來管理記憶體的函數卻十分完善。WinCE實現了Microsoft Windows XP和Microsoft Windows Me中可用到的幾乎全部的Win32記憶體管理API。WinCE支援虛擬記憶體(virtual memory)分配,本地(local)和分離(separate)的堆(heaps),甚至還有(memory-mapped files)記憶體對應檔。 像Windows XP一樣,WinCE支援一個帶有應用程式間記憶體保護功能的32位平面地址空間,但是WinCE是被設計來應用於不同場合,所以它底層的記憶體結構不同於Windows XP。這些不同能夠影響到你如何設計一個WinCE 應用程式。在這一章中,我將講述最基礎的WinCE記憶體結構。我也將講述包括WinCE中可用的記憶體配置方式中的不同點以及如何使用這些不同的記憶體類型來最小化你的程式的記憶體佔有率。
記憶體基礎 對所有的電腦來說,系統地運行一個WinCE,需要ROM(唯讀記憶體)和RAM(隨機儲存空間)。但不論如何,在WinCE系統中,ROM和RAM的使用還是稍微有些不同於個人電腦環境。
關於RAM RAM在WinCE 系統中被分為兩個地區:第一個是程式的儲存區(program memory),也叫做系統堆(system heap)。第二個是Object Storage Service區(object store)。這個Object Storage Service區有點像一個永久的虛擬RAM磁碟。不同於PC上的舊式的虛擬RAM磁碟,Object Storage Service區保留儲存的檔案甚至當系統被關閉以後。(腳註)這種安排的原因是WinCE 系統,例如Pocket PC代表性地具有一個主電池和一個備用電池。當使用者更換主電池的時候,備用電池的工作是提供電源給RAM以便維持檔案在Object Storage Service區的儲存。當使用者按了重啟鍵之後,WinCE核心就開始尋找在關閉系統前建立的Object Storage Service區,如果找到的話就將繼續使用它。 RAM中的另一個地區則用作程式儲存區。程式儲存區有點像個人電腦中的RAM,它為正在啟動並執行應用程式儲存堆和棧的內容。在Object Storage Service區和程式儲存區之間的分界線是可以通過移動它來改變的,使用者可以在控制台中找到改變這條分界線的設定。在可用記憶體降低的(low-memory)條件下,系統將會彈出對話方塊詢問使用者是否要將Object Storage Service區RAM劃分一些給程式儲存區RAM以滿足要啟動並執行應用程式的需求。
關於ROM 在個人電腦中,ROM是用來儲存BIOS(基本輸出入系統 (BIOS))並且只有64-128KB。在WinCE系統中,ROM大小可以從4MB到32MB並且存放整個作業系統以及和系統捆綁在一起的應用程式。在這種情況下,ROM在WinCE系統中就好像一個唯讀硬碟。 在一個WinCE系統中,儲存在ROM之上的程式能夠以現場執行(Execute in Place,XIP)的方式運行。換句話說,程式可以直接從ROM中執行而不必先載入到RAM中再執行。這種能力對小型系統來說,使之在兩個方面具有巨大的優勢。代碼直接從ROM中執行意味著程式碼不會佔據更有價值的RAM。同樣,程式在執行前也不必先複製到RAM中,這樣就只需要很少的時間來啟動一個應用程式。不在ROM中,但是被包含在Object Storage Service區(譯者註:上文將Object Storage Service區比作永久的RAM磁碟,故此處要說明,只有Intel力推的nor flash memroy類型才能以XIP方式執行,ROM其實也是一種nor flash memory類型)或快閃記憶卡(Flash memory storage card)中的程式將不能以現場方式執行,它們將被複製到RAM中再執行。
關於虛擬記憶體 WinCE 實現了系統的虛擬記憶體管理,在一個虛擬記憶體系統中,應用程式主要處理這個分離(譯者註:物理上可能分離,但系統將它們聯絡起來),虛擬地址空間,因此並不涉及到由硬體管理的實體記憶體。作業系統使用微處理器的記憶體管理單元來處理虛擬位址和物理地址間的即時轉換。 這種虛擬記憶體方法的優勢能從MS-DOS系統複雜的地址空間看出來。一旦請求的RAM超過最初PC設計的640-KB限制,程式設計者將不得不作出像擴充記憶體一樣的計劃以便增加可用記憶體的數量。OS/2 1.x(譯者註:IBM研製的作業系統)和Windows 3.0採用了一種基於段(segment-based)的虛擬記憶體系統來解決問題。應用程式使用虛擬記憶體不需要知道實際實體記憶體的位置,只要有記憶體可用就行。在這些系統中,虛擬記憶體以一種段的方式被實現了,即可移動的記憶體塊(譯者註:段其實就是記憶體分塊)大小從16位元組到64KB。64-KB的限制並不是由於段本身原因,而是由於Intel 80286的特性所致,這就是Windows3.x和OS/21.x的分段式虛擬記憶體系統結構。
分頁儲存Intel 80386支援的段大小已經超過64KB,但是Microsoft和IBM開始設計OS/2 2.0,他們選擇了一種不同的虛擬記憶體系統,隨後也被386所支援,這就是分頁式虛擬記憶體系統。在一個分頁儲存的系統中,最小的可被微處理器管理的單元是頁(page)。對於Windows NT和OS/2 2.0系統來說,頁大小都被設定為386處理器預設的4096位元組。當一個應用程式存取一個頁的時候,微處理器將轉換該頁的虛擬記憶體地址到實際的ROM或RAM中的物理頁(譯者註:這就是實現了地址映射和轉換,將虛擬和實際的儲存單元一一對應),這一頁同時被標記以便其他程式對該頁的訪問將被排斥。作業系統決定虛擬記憶體頁是否有效,如果有效,將做一個實體記憶體頁到虛擬頁的映射。WinCE實現了一個和其他Win32作業系統類似的分頁式虛擬記憶體系統。在WinCE中,一頁的大小可以從1024位元組到4096位元組,基於微處理器的不同而不同。這和Windows XP不同,Windows XP頁面尺寸是Intel微處理器所支援的4096位元組。對WinCE所支援的CPU類型來說,有486,Intel的Strong-ARM,和Hitachi SH4 都是是用了4096-byte 的頁面。NEC 4100在Windows CE 3.0中使用了4-KB的頁面尺寸但是在較早期的開放式系統版本中使用了1-KB的頁面大小。虛擬記憶體頁可以處在三種狀態:自由(free),保留(reserved),或被提交(committed),)。自由頁就像它的名稱一樣,自由並且可被分配。保留頁是虛擬位址已經被保留,並且不能被作業系統或進程中的其他線程重新分配。保留頁不能用在別處,但是它同樣不能被當前程式使用,因為它沒有被映射到實體記憶體。要想執行映射,它必須被提交,一個提交頁能被應用程式保留,並且直接映射到物理地址。所有我剛才講述的內容對有經驗的Win32 程式員們來說是些陳舊的知識。對Windows CE 程式員來說最重要的東西是學習Windows CE 是如何改變這些因素的。當Windows CE 實現了大部分和它的老大哥Win32一樣的記憶體API集的時候,Windows CE下面的基礎結構將影響到上面的程式。在分開來看Window CE 應用程式的記憶體結構之前,讓我們先來看看一些提供系統記憶體全域狀態的函數。
查詢系統的記憶體 如果一個應用程式知道系統當前的記憶體狀態,它將可以較好地管理可用到的資源。WinCE實現了Win32的GetSystemInfo和GlobalMemoryStatus函數,GetSystemInfo函數原型如下:VOID GetSystemInfo (LPSYSTEM_INFO lpSystemInfo);它傳遞了一個指標給SYSTEM_INFO結構,定義如下
wProcessorArchitecture參數表示系統微處理器的架構。它的值是定義在Winnt.h中,例如PROCESSOR_ARCHITECTURE_INTEL。Windows CE擴充了這些常數,包括PROCESSOR_ARCHITECTURE_ARM,PROCESSOR_ARCHITECTURE_SHx,等等。增加的常數包括像Win32作業系統支援的網路CPU(net CPU)。跳過一些參數,我們看dwProcessorType參數,它來自於特定的微處理器類型。常數有Hitachi SHx架構中的PROCESSOR_HITACHI_SH3和PROCESSOR_HITACHI_SH4。最後兩個參數,wProcessorLevel和wProcessorRevision,指明了CPU類型的特徵。wProcessorLevel參數類似於dwProcessorType參數,它一個指定的微處理器系列中被定義了,dwProcessorRevision告訴你模式(model)和晶片的步進層級(stepping level)。
typedef struct { WORD wProcessorArchitecture; WORD wReserved; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD dwActiveProcessorMask; DWORD dwNumberOfProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision;} SYSTEM_INFO; |
dwPageSize參數說明了微處理器頁面的大小,以位元組為單位。知道這個值,將會在你直接處理虛擬記憶體API的時候帶來方便,在此我只作簡短說明。lpMinimumApplicationAddress和lpMaximumApplicationAddress參數說明了應用程式可用到的最小和最大的虛擬記憶體地址。dwActiveProcessorMask和dwNumberOfProcessors參數顯示被Window XP系統支援的多個處理器數量。因為Windows CE 只支援一個處理器,所以你可以忽略這個參數。dwAllocationGranularity參數說明了一個完整的虛擬記憶體地區分配的界限。像Windows XP,Windows CE 規定虛擬區為64-KB的界限(譯者註:作者此處64-KB的意思是即使你只分配一個位元組的記憶體,系統也將會保留64-KB的虛擬位址空間給它,這個值一般是由硬體代碼實現的,但是不同硬體可能不同值)。 第二個方便的檢測系統狀態的函數如下:void GlobalMemoryStatus(LPMEMORYSTATUS lpmst);
它返回一個MEMORYSTATUS結構,定義為
typedef struct { DWORD dwLength; DWORD dwMemoryLoad; DWORD dwTotalPhys; DWORD dwAvailPhys; DWORD dwTotalPageFile; DWORD dwAvailPageFile; DWORD dwTotalVirtual; DWORD dwAvailVirtual; } MEMORYSTATUS; |
dwLength參數在調用這個函數之前必須初始化。dwMemoryLoad參數是一個不確定的值;這是一個可用的一般性的參數指示了當前系統的記憶體使用量情況(譯者註:該參數是一個近似的百分比值,指明了實體記憶體的使用方式)。dwTotalPhys和dwAvailPhys參數指明了RAM有多少頁被分配給了程式儲存區RAM,和還有多少可用(譯者註:實際上是以位元組為單位)。這些值不包括指派至儲存區的RAM。
dwTotalPageFile和dwAvailPageFile參數在Windows XP下和Windows Me下指明了當前分頁檔(paging file)的狀態。因為Windows CE不支援分頁檔,所以這些參數總是0。dwTotalVirtual和dwAvailVirtual參數指明了總共的和可用的可被應用程式存取的虛擬記憶體頁的數量(譯者註:參數都是以位元組為單位,而不是指頁數,dwAvailVirtual是指未保留和未提交的記憶體)。
通過GlobalMemoryStatus返回的資訊可以驗證Windows CE記憶體結構,通過在有32MBRAM的HP iPaq Pocket PC上調用函數,傳回值如下:
dwMemoryLoad 0x18 (24) dwTotalPhys 0x011ac000 (18,530,304)dwAvailPhys 0x00B66000 (11,952,128)dwTotalPageFile 0dwAvailPageFile 0dwTotalVirtual 0x02000000 (33,554,432)dwAvailVirtual 0x01e10000 (31,522,816) |
dwTotalPhys參數表明了系統的32MB RAM,分配了18.5MB給程式儲存區RAM,其中12MB仍然可用。注意這對應用程式來說,並不是通過這次調用,就知道了另外14MB的RAM是分配給Object Storage Service區的。要檢測分配給Object Storage Service區的RAM的大小,要使用GetStoreInformation。 dwTotalPageFile和dwAvailPageFile參數是0,表明分頁檔不被Windows CE所支援。dwTotalVirtual參數十分有趣,因為它顯示了Windows CE 強制給程式的32-MB的虛擬記憶體限制。其間,dwAvailVirtual參數顯示了只使用了32MB虛擬記憶體的一小部分(譯者註:即33,554,432-31,522,816=2,031,616)。
應用程式的地址空間儘管和Windows XP的應用程式設計類似,但Windows CE應用程式地址空間有一個巨大的不同影響到應用程式。在Windows CE之下,一個應用程式被限制在虛擬記憶體空間中它自己的32MB slot(槽)和 32MB 的slot 1中,slot 1用來載入基於XIP的DLL(譯者註:Windows CE將虛擬位址空間分為33個slot,每個slot 32MB,序號從0-32 ,附插圖c-1,c-2)。當系統只有4MB RAM的時候,分配給應用程式32MB的虛擬位址空間看起來是比較合理的,Win32的程式員在使用這個2-GB的虛擬位址空間的時候,必須記住對Windows CE應用程式的虛擬位址空間限制。圖7-1展示了一個應用程式的64-MB虛擬位址空間,其中包括32MB用於XIP的DLL空間。圖7-1 Windows CE的記憶體映射圖 要注意的是應用程式是以一個64-KB的記憶體地區開始從0x10000映射,記得最低的64KB地址空間是由Windows為所有應用程式保留的。檔案映象包括代碼,待用資料段和資源段。在實際過程中,當應用程式啟動時字碼頁(code pages)不會載入進來,只有在需要該頁面被載入的時候,代碼才被載入進來。 唯讀待用資料段(read-only static data segment)和可讀寫待用資料區(read/write static data areas)只佔很少的頁面。這些段都是排列在一起的。如同代碼一樣,只有當這些資料區段被應用程式讀或者寫的時候才會提交給RAM。應用程式的資源將被載入到一些分離的頁面中,這些資源是唯讀,並且只有當它們被應用程式擷取的時候才會分頁進入RAM。 應用程式的棧(stack)被映射到資源段之上。棧的段位置很容易被找到,因為它提交的頁就在被保留的地區的尾部。棧的表現是從高地址到低地址增長(譯者註:即從高到低填滿地址)。如果該應用程式有超過一個線程,那麼應用程式的地址空間就會保留超過一個的棧的段。 緊接著棧的就是本地堆(local heap)。引導程式保留了大量的頁,大約有幾百K交給heap來使用,但是只提交滿足malloc,new,LocalAlloc函數調用分配的記憶體(譯者註:這裡是說,被分配多少記憶體才可以提交多少記憶體,沒被分配的不能用作提交)。從本地堆的保留頁尾部到non-XIP DLL開始的部分剩餘保留頁面將被映射為自由的保留空間,如果RAM允許,應用程式可以提交這些保留頁。Non-XIP DLLs 就是不能被在ROM中現場執行的DLL將被從上至下載入到32MB的地址空間。Non-XIP DLLs 包括那些被壓縮儲存在ROM中的DLL。被壓縮的ROM 中的檔案在被載入到RAM中執行前必須先解壓縮。 被保留給XIP DLLs的32MB 應用程式地址空間的較高位置。Windows CE 映射這些XIP DLLs的代碼進入這個空間(譯者註:即較高空間),而可讀寫段被映射進較低位置。從Windows CE 4.2開始,在ROM中的純資源的DLL將被載入到應用程式64MB空間之外的虛擬記憶體空間。 腳註在PocketPC這樣的移動式系統中,當使用者按下關閉按鈕時系統將不會被真正的關閉,系統進入一種低功耗的掛起狀態。
記憶體配置的不同類型 一個Windows CE 應用程式有許多不同的記憶體配置方式。在記憶體食物鏈的底端是Virtualxxx 函數,它們直接保留,提交和釋放(free)虛擬記憶體頁。接下來的是堆(heap) API。堆是系統為應用程式保留的記憶體地區。堆有兩種風味:當應用程式啟動時自動預設分配的本地堆(local heap),以及能夠由程式手動建立的分離堆(separate heap)。在堆API之後是待用資料,資料區塊是被編譯器定義好的或者由程式手動建立的。最後,我們來看棧,這是程式為函數儲存變數的地區。 一個Windows CE不支援的Win32 記憶體API是全域堆(global heap)。全域堆API包括GlobalAlloc,GlobalFree和GlobalRealloc,將不會出現在Windows CE中(譯者註:很奇怪,我在Windows CE 中仍然可以使用這幾個API,並且工作正常,好像Microsoft並沒有把它們完全去掉)。全域堆只是從Windows 3.x的Win16 時期繼承而來。在Win32中,全部和本地的堆很類似,全域記憶體一個獨特用法是,為剪貼簿的資料分配記憶體,在Windows CE中已經被本地堆替代並加上了控制代碼。 在Windows CE中最小化記憶體使用量的關鍵是選擇與記憶體塊使用模型相匹配的恰當的記憶體配置策略。我將回顧一下這些記憶體類型然後講述Windows CE應用程式中的最小化記憶體使用量策略。
虛擬記憶體 虛擬記憶體是記憶體類型中最基礎的。系統調用虛擬記憶體API來為其他類型記憶體配置記憶體。包括堆和棧。虛擬記憶體API,包括VirtualAlloc,VirtualFree和VirtualReSize函數,這些可以直接操作應用程式虛擬記憶體空間的虛擬記憶體頁面。頁面可以保留,提交給實體記憶體,或使用這些函數釋放。
分配虛擬記憶體
分配和保留虛擬記憶體是同過這個函數完成的:
LPVOID VirtualAlloc (LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect); |
VirtualAlloc 的第一個參數是要分配記憶體地區的地址。當你使用VirtualAlloc來提交一塊以前保留的記憶體塊的時候,lpAddress參數可以用來識別以前保留的記憶體塊。如果這個參數是NULL,系統將會決定分配記憶體地區的位置,並且圍繞64-KB的範圍(譯者註:就是前面說提及的最小記憶體配置尺寸)。第二個參數是dwSize,要分配或者保留的地區的大小。這個參數以位元組為單位,而不是頁,系統會根據這個大小一直分配到下頁的邊界。
flAllocationType參數指定了分配的類型,你可以指定或者合并以下標誌:MEM_COMMIT,MEM_AUTO_COMMIT,MEM_RESERVE和MEM_TOP_DOWN。MEM_COMMIT標誌分配程式使用的記憶體,MEM_RESERVE保留虛擬位址空間以便以後提交。保留的頁不能存取直到調用VirtualAlloc的時候再次指定了MEM_COMMIT標誌。第三個標誌,MEM_TOP_DOWN,告訴系統從最高可允許的虛擬位址開始映射應用程式。The MEM_AUTO_COMMIT標誌是唯一一個Windows CE最方便的標誌,當這個參數被指定了之後,記憶體塊立即被保留,當其中的頁被第一次存取的時候,系統將自動認可該頁。這允許你分配大塊的虛擬記憶體而不需要顧及系統和實際RAM分配直到當前頁被第一次使用。自動認可記憶體的缺點是,物理RAM需要退回當頁面被第一次訪問時可能停用頁面。在這種情形下,系統將產生一個異常(exception)(譯者註:可能會出現因為無法訪問而出錯)。 VirtualAlloc可以通過並行多次調用提交一個地區的部分或全部來保留一個大的記憶體地區。多重調用提交同一塊地區不會引起失敗。這使得一個應用程式保留記憶體後可以隨意提交將被寫的頁。當這種方式不在有效時候,它會釋放應用程式通過檢測被保留頁的狀態看它是否在提交調用之前已經被提交。 flProtect參數指定了被分配地區的訪問保護方式。這些不同的標誌被總結在下面的列表中:PAGE_READONLY該地區為唯讀。如果應用程式試圖訪問地區中的頁的時候,將會被拒絕訪問。PAGE_READWRITE地區可被應用程式讀寫。PAGE_EXECUTE地區包含可被系統執行的代碼。試圖讀寫該地區的操作將被拒絕。PAGE_EXECUTE_READ地區包含可執行代碼,應用程式可以讀該地區。PAGE_EXECUTE_READWRITE地區包含可執行代碼,應用程式可以讀寫該地區。PAGE_GUARD地區第一次被訪問時進入一個STATUS_GUARD_PAGE異常,這個標誌要和其他保護標誌合并使用,表明地區被第一次訪問的許可權。PAGE_NOACCESS任何訪問該地區的操作將被拒絕。PAGE_NOCACHERAM中的頁映射到該地區時將不會被微處理器緩衝(cached)。PAGE_GUARD和PAGE_NOCHACHE標誌可以和其他標誌合并使用以進一步指定頁的特徵。PAGE_GUARD標誌指定了一個防護頁(guard page),即當一個頁被提交時會因第一次被訪問而產生一個one-shot異常,接著取得指定的存取權限。PAGE_NOCACHE防止當它映射到虛擬頁的時候被微處理器緩衝。這個標誌方便裝置驅動使用直接記憶體存取方式(DMA)來共用記憶體塊。