天天看點

Windows CE .NET 進階記憶體管理

<!--[if !vml]-->

<!--[endif]-->show toc

摘要

Microsoft_ Windows CE 的優點之一是它的 Microsoft Win32_ 應用程式程式設計接口 (API) 支援。無數 Windows 程式員可以利用他們的 Win 32 API 和 MFC 知識相對容易地轉移到 Windows CE。Windows CE 可以實作 Win32 API 的子集,但程式員應當永遠不要忘記 Windows CE 是與 Windows XP 完全不同的作業系統,Windows CE 具有不同的要求和不同的實作。在設計應用程式或診斷問題時知道 Windows CE 如何實作它的 Win32 相容性是一件非常重要的事情。

記憶體管理是 Windows CE 和 Windows XP 之間在實作方面存在很明顯差異的地方之一。雖然 Windows CE 支援幾乎每個 Win32 記憶體管理函數(除了我們不贊成對它使用全局堆函數),但是這些記憶體管理 API 的實作是完全不同的。這些差異可以妨礙那些不熟悉 Windows CE 與 Windows 的桌面版本之間的細微差異的程式員。要了解這些問題位于哪裡,必須首先了解 Windows CE 如何管理記憶體。

系統記憶體映射

Windows XP 和 Windows CE 都是 32 位作業系統,都同樣支援 4 GB 的虛拟位址空間。Windows XP 将位址空間劃分為兩個 2 GB 區域。上半部位址空間是為系統保留的。下面 2 GB 位址空間則由每個正在運作的應用程式重複使用。

<!--[if !vml]-->

Windows CE .NET 進階記憶體管理

<!--[endif]-->

圖 1. Windows XP 虛拟記憶體空間

第一眼看,Windows CE 的虛拟位址空間是以類似系統保留區域的方式組織的,并且是重複的應用程式空間。圖 2 顯示了 Windows CE 位址空間。在這裡,上部 2GB 位址空間也是為系統保留的。下半部位址空間則劃分為很多區域。該區域的大多數(幾乎一半空間)被定義為大型記憶體區域 (Large Memory Area)。該區域用來配置設定通常用于記憶體映射檔案的大型記憶體空間塊。

在大型記憶體區域的下面是另一個大型區域,本文稱為保留區域。在保留區域的下面,在記憶體空間的最底端,是一個 64 MB 的區域。該 64 MB 區域,更準确地說是最下面的 32 MB 區域,是每個正在運作的應用程式重複使用的區域。

<!--[if !vml]-->

Windows CE .NET 進階記憶體管理

<!--[endif]-->

圖 2. Windows CE 虛拟記憶體空間

Windows CE 應用程式記憶體映射

最下面的 64 MB 虛拟位址空間是駐留 Windows CE 應用程式的地方。圖 3 顯示了該應用程式虛拟位址空間。像在 Windows XP 應用程式中一樣,應用程式代碼從虛拟位址 0x10000 開始加載。應用程式啟動時,将在位址空間中為所有代碼保留足夠的空間。然後,在需要實際代碼時,這些代碼将被按需分頁排程進該位址空間。

在為代碼保留的區域上面,頁是為隻讀和讀/寫靜态資料區域保留的。此外,還為本地堆和應用程式中運作的每個線程的堆棧保留了區域。當線程啟動時,為每個堆棧保留的區域的大小是固定的。隻在堆棧增長時才會送出實際的 RAM。另一方面,堆保留了需要在堆中配置設定 RAM 塊時增長的區域。

當加載“現場執行”(XIP) DLL 時,将從 64 MB 空間的頂部向下加載這些 DLL。當建立 ROM 時,每個 XIP DLL 都會被定址(确定在位址空間中的位置)。加載非 XIP DLL 時,将把它放在 32 MB 邊界的下面。非 XIP 的 DLL(也叫作基于 RAM 的 DLL)是指那些從對象存儲區加載的 DLL、從 ROM 解壓縮的 DLL或從外部檔案系統(例如 Compact Flash 卡)加載的 DLL。應用程式虛拟記憶體空間中靠上位置的 32 MB 僅用于 XIP DLL。

<!--[if !vml]-->

Windows CE .NET 進階記憶體管理

<!--[endif]-->

圖 3. Windows CE .NET 應用程式虛拟記憶體空間

由應用程式通過建立單獨的堆或直接調用 VirtualAlloc API 所配置設定的任何其他記憶體将從底部向上進行配置設定,配置設定時,系統将查找第一個足夠大、可滿足配置設定的可用區域。

限制因素

盡管 Windows CE 應用程式的一個限制因素是可用于應用程式的 RAM 數量,但還有另一個主要的限制是應用程式的相對很小的 32 MB 虛拟位址空間。盡管 XIP DLL 被加載在 32 MB 空間的上端,但所有其他記憶體配置設定和任何基于 RAM 的 DLL 都必須能夠放進應用程式的 32 MB 記憶體空間中。這個 32 MB 限制“框”不是 Windows CE 程式員所面臨的很大問題,因為它是一個可被克服的挑戰。

要了解這個看似很大的記憶體空間有些什麼限制性,必須了解 VirtualAlloc API 的操作原理。VirtualAlloc 是任何 Microsoft Win32 作業系統中最基礎的記憶體配置設定調用。它在頁級别配置設定記憶體;頁是可以被 CPU 配置設定或釋放的最小的記憶體機關。Windows CE .NET CPU 的頁大小是 1024 或 4096 位元組,這取決于 CPU。最廣泛使用的是 4 KB 頁大小。

VirtualAlloc 調用配置設定記憶體的過程分為兩個步驟。第一步,保留虛拟記憶體空間的區域。這種保留不會消耗任何 RAM;它隻是防止一部分虛拟位址空間被用于其他用途。保留記憶體空間之後,就可以送出 (commit) 部分或整個區域,這個過程是指将實際實體記憶體映射到保留區域。VirtualAlloc 函數用于保留記憶體空間和送出記憶體。下面顯示了 VirtualAlloc 函數的原型。

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_COMMIT 标志用于配置設定程式使用的記憶體。MEM_RESERVE 用于保留要随後送出的虛拟位址空間。保留頁是無法通路的,直到通過指定區域并使用 MEM_COMMIT 标志進行了另一個 VirtualAlloc 調用為止。MEM_AUTO_COMMIT 标志唯一用于 Windows CE 并且很好用,但它不是本文的主題。

是以,要使用 VirtualAlloc 來配置設定可使用的 RAM,應用程式必須調用 VirtualAlloc 兩次,一次保留記憶體空間,再一次則送出實體 RAM;或者調用 VirtualAlloc 一次,這需要在 flAllocationType 參數中組合使用 MEM_RESERVE 和 MEM_COMMIT 标志。

組合保留和送出标志方式所使用的代碼更少,并且更快、更簡單。該技術通常用在 Windows XP 應用程式中,但用在 Windows CE 應用程式中不是很好。以下代碼片段示範了存在的問題。

INT i;

PVOID pMem[512];

for (i = 0; i < 512; i++) {

   pMem[i] = VirtualAlloc (0, PAGE_SIZE, MEM_RESERVE | MEM_COMMIT,

                           PAGE_READWRITE);

}

該代碼片段似乎是無害的。它配置設定了 512 塊記憶體,每塊記憶體的大小為 1 頁。問題是:在 Windows CE 系統上,甚至是在有數兆位元組可用 RAM 的系統上,該代碼總是會失敗。問題在于 Win32 作業系統保留記憶體區域的方式。

在任何 Win32 作業系統(包括 Windows CE .NET)上,當一個虛拟記憶體空間區域被保留時,它會将保留區域對齊 64 KB 邊界。因而,上面的代碼片段試圖将保留的 512 個區域的每個區域都對齊 64 KB 邊界。Windows CE 應用程式的問題是它們必須位于 32 MB 虛拟記憶體空間的範圍内。在整個應用程式記憶體空間中該空間的大小隻有 512 64 KB,并且它們中的一部分需要用作應用程式代碼、本地堆、堆棧和應用程式所加載的每個 DLL 的區域。通常,在對 VirtualAlloc 進行大約 470 次調用之後上面的代碼片段将失敗。

上述問題的解決方案是首先保留足夠用于整個配置設定的較大區域,然後在需要時送出 RAM,如下所示。

INT i;

PVOID pBase, pMem[512];

pBase = VirtualAlloc (0, 512*PAGE_SIZE, MEM_RESERVE, PAGE_READWRITE);

for (i = 0; i < 512; i++) {

   pMem[i] = VirtualAlloc (pBase + (i * PAGE_SIZE), PAGE_SIZE,

                           MEM_COMMIT, PAGE_READWRITE);

}

避免該問題的關鍵是知道這個情況。這隻是 Windows CE 應用程式的位址空間中隻有 512 個區域所帶來的問題影響應用程式的很多地方中的一個。

配置設定大型記憶體塊

Windows CE .NET 應用程式的位址空間局限于 32 MB 所引起的另一個問題是如何配置設定大型記憶體塊。當應用程式的整個位址空間被限制在 32 MB 以内時,如果應用程式需要一塊 8、16 或 32 Mb RAM 用于具體用途,它怎樣才能配置設定該記憶體?回答是應用首先用在 Windows CE .NET 早期版本中針對視訊驅動程式的一個修複程式。有了它,如果 Windows CE .NET 檢測到一個對 VirtualAlloc 的調用請求保留超過 2 MB 的位址空間,該位址空間将不會保留在 32 MB 的限制大小中。該記憶體塊将保留在大型記憶體區域 (Large Memory Area) 中,大型記憶體區域位于全局記憶體空間中,正好在 2 GB 系統保留白間的下面。

記憶體空間已經保留後,應用程式就可以通過調用 VirtualAlloc 來送出在保留白間内的具體頁。這就允許應用程式或驅動程式使用大型記憶體塊,即使它存活在 32 MB 的大小的限制内。下面的代碼顯示了配置設定 64 MB 塊然後送出保留區域的一頁的簡單情形。

   PVOID ptrVirt, ptrMem;

   ptrVirt = VirtualAlloc (0, 1024 * 1024 * 64, MEM_RESERVE,

                        PAGE_NOACCESS);

   if (!ptrVirt) return 0;

   ptrMem = VirtualAlloc ((PVOID)((int)ptrVirt+4096),

                          4096, MEM_COMMIT, PAGE_READWRITE);

   if (!ptrMem) {

      VirtualFree (ptr, 0, MEM_RELEASE);

      return 0;

   }

   return ptrMem;

前面的代碼還顯示了直接處理虛拟記憶體 API 所具有的一個特性。這就是您可以建立大型稀疏數組,而不會消耗大量 RAM。在上面的代碼中,64 MB 保留區域不會消耗任何實體 RAM。在該示例中,唯一被消耗的 RAM 是在第二次調用 VirtualAlloc 以送出頁時使用的一個頁(4096 位元組)。

DLL 加載問題

目前,有很多在 Pocket PC 2002 上程式設計的 Windows CE 程式員。有一個重要問題會影響 Pocket PC 2002 程式員,這個問題與應用程式加載 DLL 有關,盡管對 Windows CE .NET 記憶體體系結構所作的更改修複了這個問題。要了解該問題,首先必須了解在 Windows CE .NET 如何不同于 Windows CE 3.0 與兩個版本的 Windows CE 如何加載和管理 DLL 之間存在的一個主要差異。

Windows CE .NET 的新功能之一是将應用程式的虛拟位址空間從 Windows CE 早期版本的 32 MB 擴充到 64 MB。虛拟空間中可用于 XIP DLL 的高端 32 MB 不可用于 Windows CE 3.0。是以,運作在基于 Windows CE 3.0 的系統上的應用程式必須将它們的 XIP DLL、它們的代碼和它們的所有資料加載到 32 MB 虛拟位址空間中。圖 4 顯示了一個 Windows CE 3.0 應用程式的應用程式記憶體空間。圖 4 展示了 Windows CE 3.0 應用程式記憶體空間的關系圖。

<!--[if !vml]-->

Windows CE .NET 進階記憶體管理

<!--[endif]-->

圖 4. Windows CE3.0 應用程式虛拟記憶體空間

因為 Pocket PC 2002 是基于 Windows CE 3.0 的,是以運作在該平台上的應用程式會受到該虛拟記憶體空間的限制。

DLL 加載

除了在加載 XIP DLL 時 Windows CE 的早期版本與 Windows CE .NET 有額外 32 MB 空間的差異以外,Windows CE 用來加載 DLL 的技術與用于 Windows CE .NET 的技術是相同的。

當發出加載一個 DLL 的請求時,核心首先檢查該 DLL 是否先前已被另一個應用程式加載,如果沒有,并且 DLL 不是 XIP DLL,則核心将使用經過修改的從上到下搜尋在 32 MB 虛拟記憶體映射中查找第一個可用的空間。搜尋被認為經過修改,是因為核心将避免使用由另一個 DLL 使用的任何位址,即使該 DLL 不是由目前程序加載的。該搜尋技術確定了将系統中的每個 DLL 加載在唯一、非重疊的位址中。

之是以必須使用唯一位址,是因為如果 DLL 是由多個程序加載的,則在所有過程中它必須位于相同的虛拟位址中。通過用唯一的位址加載每個不同的 DLL,核心可以確定如果應用程式想加載由另一個程序先前加載的 DLL,則在其他程序中 DLL 所映射的虛拟位址可用于請求該 DLL 的程序。圖 5 顯示了三個程序分别加載一系列 DLL 的關系圖。在該圖中,DLL A 由所有三個程序加載在相同位址上。程序 2 加載 DLL C,後者位于比程序 1 所加載的 DLL B 和 DLL A 更低的位址空間中。程序 C 随後加載 DLL A 和它自己的 DLL D。注意,在每個程序中,相同的 DLL 加載在相同的位址中,而每個不同的 DLL 則加載在唯一的位址中。

<!--[if !vml]-->

Windows CE .NET 進階記憶體管理

<!--[endif]-->

圖 5. 加載一系列 DLL 的三個程序

現在考慮如果遇到潛在的問題該怎麼辦。假設程序 2 加載了很大的 DLL C(如圖 6 所示)。注意,程序 3 正好是一個大型 .exe 檔案,并且在程序 2 已加載了相當大的 DLL C 之後程序 3 也要加載 DLL。很顯然,如果程序 3 試圖加載還沒有被其他程序加載的任何其他 DLL,它很可能遇到麻煩。該示例有點故意設計的成分,因為 DLL C 必須具有難以置信的大小,或者程序 2 必須加載大量 DLL,之後該問題才會自然發生。

<!--[if !vml]-->

Windows CE .NET 進階記憶體管理

<!--[endif]-->

圖 6. 三個程序加載一系列 DLL,其中程序 2 加載大型 DLL

讨論了正常加載 DLL 後,現在是讨論如何處理複雜的 XIP 和非 XIP DLL 的時候了。當 OEM 建立 ROM 鏡像時,每個現場執行 DLL 都将被定址在一個唯一位址上。以這種方式,所有 XIP DLL 就能在互相不發生沖突的情況下被加載。因為它們是 XIP,是以包含 DLL 代碼的 ROM 可以直接映射到請求它的任何應用程式的虛拟位址空間。XIP DLL 在被程序加載時不能再定址到另一個位址,因為更改基址将涉及修改隻讀代碼。

核心在為非 XIP DLL 查找可用的虛拟位址時,它會從最低定址 XIP DLL 的下面開始搜尋可用的虛拟位址。這不是應用程式已加載的最低定址 XIP DLL,而是整個系統中的最低定址 XIP DLL,無論它是否是由任何應用程式加載的。在這裡,該技術再次保證了目前加載的每個 DLL 都可以被其他程序加載。盡管該系統運作得很好,但因為某個 DLL 在 Pocket PC 2002 的 Windows CE .NET 中可能隻能有唯一的實作,是以有時 DLL 不會被其他程序加載。

在 Pocket PC 2002 上的 Windows CE .NET 實作利用了 Windows CE 3.0 中的功能,該功能允許在裝置上使用多個 ROM。該功能允許在系統中使用多個 ROM,即使它們沒有連續的實體位址。

上面已經提到,DLL 需要經過特殊的處理才能成為 XIP。因為對 DLL 的定址需要更改 DLL 的代碼,是以在建立 ROM 鏡像時 DLL 必須被定址。建立第一個 ROM 時,ROM 建立工具将對每個 DLL 定址,以便它不會與 ROM 中的任何其他 DLL 發生重疊。

使用多個 XIP 區域意味着 DLL 加載問題需要核心設計者重新考慮。要確定在多個 XIP 區域系統上 XIP DLL 永遠不會重疊,必須将第二個 ROM 上的 DLL 定址到比第一個 ROM 鏡像的最低 DLL 更低的虛拟位址。如果使用了其他 ROM,則這些 XIP 區域中的 DLL 還必須定址到比前一個 ROM 更低的位址。

由于其他原因,使用多個 ROM 鏡像很容易。如果 OEM 或 Microsoft 想更新 Windows CE 鏡像的一部分,它們可以為具體 ROM 發出更新,而不必更新整個系統。為了保證一個 ROM 的更新不需要有對另一個 ROM 的更改,Microsoft 鼓勵不要将定址于較低鏡像中的 DLL 定址到前一個鏡像中最低 DLL 的位址,而應當定址在比它更低的位址上,以在一組 DLL 和另一組 DLL 之間人為引入虛拟記憶體空隙。

負責 Pocket PC 2002(基于 Windows CE 3.0)的 Microsoft 内部開發人員最大限度地利用了多個 XIP 區域。大多數 Pocket PC 實作都有五個或更多個 XIP 區域。問題是區域之間的空隙太大。Pocket PC 2002 鏡像中的最低定址 XIP DLL 通常定址在 0x0100000 以下。因為 Windows CE 将基于 RAM 的 DLL 放在最低 XIP DLL 的下面,是以可供基于 RAM 的 DLL、應用程式代碼、它的堆和堆棧使用的空間沒有限制在 32 MB 虛拟位址空間的範圍内,而是在最低 XIP DLL 下面的空間中(小于 16 MB)。

圖 7 顯示了 Pocket PC 2002 的問題。注意,XIP DLL 的虛拟記憶體空間中的區域相當大。事實上,這幅圖很保守,因為它沒有顯示 XIP 區域接管虛拟記憶體空間的一半的情形,而 Pocket PC 2002 上通常就是這種情況。注意基于 RAM 的 DLL 的加載;A、B、C 和 D 位于虛拟位址空間中低很多的位置。

<!--[if !vml]-->

Windows CE .NET 進階記憶體管理

<!--[endif]-->

圖 7. 在 Pocket PC 2002 上加載 DLL,大部分虛拟位址空間被 XIP DLL 使用

對于處理海量資料的公司應用程式,公司開發人員被迫在他們的 Windows CE 應用程式中使用大型資料庫。通常資料庫引擎被實作為 DLL,而它通常很大。在上面的示例中,資料庫 DLL 是制造麻煩的 DLL C。可用于 Pocket PC 2002 應用程式的虛拟記憶體空間小于 16 MB,而人們又需要大型的、基于 RAM 的 DLL,這使得很多開發人員發現他們的應用程式将由于缺少空間而無法運作 — 不是缺少 RAM,而是虛拟記憶體空間。

組合 DLL

可用來減輕 Pocket PC 2002 上的該問題的技術有不少。首先,開發人員應當通過将小型 DLL 組合成更大的 DLL 來減少 DLL 的數目。每個 DLL 至少占據一個 64 KB 區域。如果應用程式有 4 個 DLL,每一個的大小是 20 KB,則 DLL 使用的總計記憶體空間是 256 KB。通過組合四個 DLL,所得到的大型 DLL 将僅消耗 64 KB 虛拟記憶體空間 — 代碼隻占用 60 KB,但最低記憶體使用量是 64 KB。正常規則是,将 DLL 組合成(但不超過)64 KB 的倍數的大小。在某些包含過多小型 DLL 的應用程式中,隻需将 DLL 組合成幾個大型 DLL 就能解決應用程式的 DLL 加載問題。

将 DLL 代碼轉移到應用程式

在 Pocket PC 2002 中減少 DLL 問題的另一個方法是将 DLL 中的代碼轉移到應用程式。即使多個程序共享代碼,有時在多個程序中複制代碼也是有利的,因為不同程序将獨立于其他應用程式被加載到記憶體中。

首先,将代碼移動到應用程式中似乎沒有幫助 — 代碼仍然在應用程式的 32 MB 虛拟空間中。但是,這裡的關鍵是要使某些代碼成為不需要大型的、基于 RAM 的 DLL 的大型應用程式,而使其他代碼成為加載和使用基于 RAM 的 DLL 的小型應用程式。在該技術中,大型應用程式執行大多數業務邏輯和用于加載大型 DLL 的小型應用程式。如果大型應用程式需要得到大型 DLL 的服務,它必須使用程序間通信讓較小的程序調用該 DLL,并通過再次使用程序間通信将資料傳回給大型程序。

定義 DLL 加載順序

減少 DLL 數或轉移應用程式的代碼還不夠,下面讨論更基本的方法:手動指定 DLL 的加載順序。加載順序是重要的,因為如果大型 DLL 在早期加載,它将迫使所有随後的小型 DLL 的加載位址向下轉移。通常,大型 DLL 被單個應用程式使用。但如果它被早期加載,它可以迫使其他應用程式 DLL 的加載位址向下轉移到無法加載這些 DLL 的位置,進而沖擊其他應用程式。

解決方案是首先加載小型 DLL,然後讓會造成影響的大型 DLL 在晚期加載,甚至最後加載。這就産生了如何強制執行 DLL 加載順序的問題。一個方式是對應用程式套件中不同程序的啟動順序進行排隊,但這有時會有問題。

另一個定義 DLL 加載順序的方式是編寫一個運作于主要應用程式之前的小型應用程式,讓它通過重複調用 Win32 函數 LoadLibrary,按定義好的順序加載基于 RAM 的 DLL。DLL 加載程式在主應用程式的生存期内一直運作,然後終止。它甚至可以通過調用 CreateProcess 來啟動主應用程式,并通過阻塞 CreateProcess 所傳回的程序句柄而進入等待狀态,直到主應用程式終止。加載 DLL 的應用程式不會使用很多 RAM,因為被加載的 DLL 最後全部都要由其他程序來加載。

為解決 Pocket PC 2002 上的 DLL 加載問題而讨論的所有解決方案都各有缺點。沒有一個是完美的或不可辯駁的。但是,它們确實是開發人員用來開發其産品的解決方案。以後釋出的 Pocket PC 應當解決該問題,但對開發 Pocket PC 2002 産品的開發人員來說,及時解決問題是關鍵的。

通過知道 Windows CE 如何管理記憶體,開發人員就能更快地避免缺陷,并診斷問題。了解 Windows CE 如何管理 DLL 将有助于避免 Pocket PC 2002 應用程式中潛在的問題。即使未來釋出的 Pocket PC 解決了該問題,已經在使用的數百萬裝置仍然需要應用程式。知道從哪裡查找問題是找到并解決問題的第一步。

繼續閱讀