Windows CE 進程、線程和記憶體管理(三)

來源:互聯網
上載者:User
三、記憶體管理

  同其它Windows作業系統一樣,Windows CE.NET也支援32位虛擬記憶體機制、按需分配記憶體和記憶體對應檔等。但是與其它Windows作業系統又有明顯的不同。畢竟Windows CE是一種嵌入式即時性的作業系統,在記憶體管理方面必須要比其它Windows作業系統更節約實體記憶體和虛擬位址空間。在記憶體管理API方面,為了便於移植程式,Windows CE和其它Windows作業系統函式宣告基本一致,這使一個在其它Windows下開發的程式員可以直接使用早就熟悉的API函數, 但是CE下記憶體管理的原理開發人員還是應該熟悉的。

1、ROM和RAM
  最早的基於Windows CE的民用產品,採用的存放裝置都是ROM + RAM ,ROM儲存CE核心檔案、應用程式,而RAM用於核心、所有應用程式運行時使用,關閉電源時必須給RAM提供電力來儲存系統配置資訊、使用者產生的檔案等。為了適應這樣的儲存硬體,CE採用了ROM檔案系統和RAM檔案系統。在ROM中存放的模組可以是壓縮的,也可以是不壓縮的,這取決於OEM。OEM在定製核心時可以設定是否壓縮模組。如果是壓縮的,模組在運行前先解壓並全部存放到RAM中。如果是不壓縮的,就本地執行(XIP,executed in place)。本地執行和其它Windows作業系統下執行應用程式、DLL方式一致,也就是應用了記憶體對應檔技術。在這裡我順便講一下。在啟動時應用程式或DLL的程式碼片段不載入到實體記憶體中,核心只是分配虛擬位址空間給程式碼片段,當執行代碼時核心會到實際存放在硬碟上的檔案中尋找代碼並執行。採用這樣的技術既可以節省可用記憶體又可以減少載入的時間。請注意,作業系統首先會到為硬碟準備的緩衝區裡讀取代碼資料,如果沒有就命令硬碟讀取應用程式檔案資料到緩衝區。所以緩衝區設定大點是有好處的。Windows CE的本地執行就是採用這樣的技術來載入ROM內的應用程式和DLL的。所以Windows CE的DLL分為XIP DLL和非XIP DLL。這種載入方式的缺點就是執行相對較慢一點,如果用PB建立一個具有即時性特點的核心,一定不能選用XIP技術。
  到後來基於Windows CE的產品開始採用FLASH、IDE等永久存放裝置時,檔案系統又加了個FAT。核心檔案和其它應用程式也可以存放到永久存放裝置中,核心由載入程式解壓並載入到RAM的Object Storage Service地區(object store),包含在核心中的所有系統應用程式檔案和DLL檔案都存放到這個地區。當執行一個應用程式時,核心將這個應用程式調用的系統DLL載入到Slot 1(0x0200 0000-0x03FF FFFF)。在Windows CE.NET中Slot 1專用於XIP DLL使用。
  RAM檔案系統專用於Object Storage Service。在以前的文章中曾經講過,它和ROM檔案系統是Windows CE預設的檔案系統。Windows CE啟動後把RAM分為Object Storage Service地區(object store)和應用程式記憶體地區(program memory)。Object Storage Service地區採用RAM檔案系統來儲存檔案,一般用於儲存核心解開的所有檔案。應用程式記憶體地區留給所有應用程式運行時使用。在Windows CE下"控制台"-"系統"-"記憶體"中,可以調節這兩個儲存地區的比例,滑塊向左,則釋放Object Storage Service地區的一些記憶體並將這些記憶體划到應用程式記憶體地區中。滑塊向右則相反。

2、記憶體結構
  Windows CE.NET只能管理512MB的實體記憶體和4GB大小的虛擬位址空間。不同的CPU記憶體管理方法也不同。對於MIPS和SHX系列CPU來說,物理地址映射是由CPU完成的,CE核心可以直接存取512MB的實體記憶體。對於x86系列和ARM系列的CPU來說,在核心啟動過程中它會將現有實體記憶體地址全部映射到0x8000 0000以上的虛擬位址空間中供核心以後使用。OEM可以通過OEMAddressTable來詳細定義虛擬位址和物理地址的映射關係。OEMAddressTable本身並不是一個檔案,它只是存在於其它檔案中描述虛擬位址和實際物理地址的映射關係的資料。比如檔案oem init.asm中包含一段代碼:dd 80000000h, 0, 04000000h 。它表示將整個物理地址(0x0400 0000=64MB)共64MB映射到虛擬位址從0x8000 0000到0x8400 0000中。關於OEMAddressTable我將在以後關於PB的文章中講述。
  整個4GB虛擬位址空間主要劃分為兩部分,從0x8000 0000以上為核心使用部分,0x8000 0000以下為應用程式使用部分。詳細見下表:

 

位址範圍 用途
0x0000 0000到0x41FF FFFF   由所有應用程式使用。共33個槽,每個槽佔32MB。槽0(Slot 0)由當前佔有CPU的進程使用。槽1由XIP DLL使用。其它槽用於進程使用,每個進程佔用一個槽。
0x4200 0000到0x7FFF FFFF   由所有應用程式共用的地區。32MB地址空間有時不能夠滿足一些進程的需求。那麼進程可以使用這個範圍的地址空間。在這個地區裡應用程式可以建堆、建立記憶體對應檔、分配大的地址空間等。
0xA000 0000到0xBFFF FFFF   在這個範圍核心重複定義0x8000 0000到0x9FFF FFFF之間定義的物理地址映射空間。區別是在這範圍映射的虛擬位址空間不能夠用於緩衝。
  我舉例來說明:假設一個產品有64MB實體記憶體。如上文所述定義好OEMAddressTable後。核心啟動後一個物理地址映射空間範圍在0x8000 0000到0x8400 0000,那麼核心會從0xA000 0000到0xA400 0000定義一個同樣範圍的地址空間,這個地址空間和0x8000 0000到0x8400 0000映射到相同的物理地址。但這個虛擬位址空間不能夠用於緩衝。
0xC000 0000到0xC1FF FFFF 系統保留空間
0xC200 0000到0xC3FF FFFF 核心程式nk.exe使用的地址空間。
0xC400 0000到0xDFFF FFFF   這個範圍為使用者定義的靜態虛擬位址空間,但這個地址空間只能用於非緩衝使用。
  利用OEMAddressTable定義物理地址映射空間後,每次核心啟動時這個範圍都不改變了,除非產品包含的實體記憶體容量發生變化。假如增加到128MB實體記憶體,那麼物理地址映射空間也向後擴大了一倍。Windows CE.NET也允許使用者建立靜態物理地址映射空間。使用者可以調用CreateStaticMapping函數或者NKCreateStaticMapping函數來映射某一段物理地址到0xC400 0000和0xE000 0000之間的某一個範圍。需要注意的是用這個函數建立的靜態虛擬位址只能夠由核心訪問,而且不能用於緩衝。
0xE000 0000到0xFFFF FFFF   核心使用的虛擬位址。當核心需要大的虛擬位址空間時,會在這個範圍內分配。


圖1 Windows CE.NET記憶體結構

3、進程地址空間結構
  進程地址空間結構2所示。這個圖源至MSDN。Windows CE.NET同以前版本的Windows CE作業系統在進程地址空間上有所不同,以前的Windows CE把XIP DLL也載入到進程的32MB地址空間中,而Windows CE.NET把XIP DLL單獨載入到Slot 1中,這樣對於每個進程來說,它總的地址空間就大了一倍,也就是64MB。這個問題我在講解進程的時候提到過。
  當一個應用程式啟動時,核心為這個程式選擇一個閒置槽(Slot),並且載入所有的代碼、資源,並分配堆棧,載入DLL等。當這個進程得到CPU使用權時,它的整個地址空間被核心映射到Slot 0,也就是當前進程使用的地址空間,然後開始運行。圖中給出的地址實際上是經過映射到Slot 0之後的結構。可以看出,進程首先載入程式碼片段,因為每個進程最低部64KB作為保留地區,所以程式碼片段從0x0001 0000開始,核心為程式碼片段分配足夠的虛擬位址空間後,接著分配空間為唯讀資料和可讀/可寫資料,接著分配空間為資源資料,之後分配空間為預設堆和棧。非XIP DLL從進程最高地址向下開始載入。非XIP DLL的載入按如下規則:核心先檢查要載入的DLL是否被其它進程載入過,如果載入過,就做一個地址的重定位。這樣就避免了整個系統內多次載入相同DLL。如果沒有載入過,就按照從槽的高地址到槽的低地址的順序尋找閒置地址空間。然後分配足夠的地址空間用於載入DLL。因為每個進程在執行前都要映射到Slot 0,而且進程使用的所有DLL可能來自不同的槽(Slot),為避免所有使用的DLL在映射到Slot 0中出現地址空間衝突的現象,核心的載入器(Loader)在載入DLL時會尋找所有槽中載入的DLL的地址,保證在映射到Slot 0時不會發生地址衝突現象。假如系統內有兩個進程,進程A只載入了DLL A,進程B需要載入DLL A和DLL B,那麼進程B會留出DLL A的地址空間,然後載入DLL B,也就是說進程B映射到Slot 0時,DLL A的地址空間和DLL B的地址空間是相鄰的,不會發生衝突。好在Windows CE下DLL都很小,而且一個應用程式使用的DLL多數是系統的DLL(存在於Slot 1)。所以目前來看進程的地址空間還夠用。


圖2 進程地址空間結構

4、堆和棧
  堆是一段連續的較大的虛擬位址空間。應用程式在堆中可以動態地分配、釋放所需大小的記憶體塊。利用堆的優點是在一定範圍內減小了記憶體碎塊。而且開發人員分配記憶體塊前不必去瞭解CPU的類型。因為不同的CPU分頁大小不相同,每個記憶體頁可能是1KB、4KB或更多。在堆內分配記憶體塊可以是任意大小的,而直接分配記憶體就必須以記憶體頁為單位。當一個應用程式啟動時,核心在進程所在的地址空間中為進程分配一個預設192KB大小的虛擬位址空間,但是並不立刻提交實體記憶體。如果在運行當中192KB不能滿足需求,那麼核心會在進程地址空間中重新尋找一個足夠大小的閒置地址空間,然後複製原來堆的資料,最後釋放原來的堆所佔的地址空間。這是因為預設的堆的高地址處還有棧,所以必須重新分配一個。Windows CE.NET的堆有明顯的缺點,不同於其它Windows作業系統下的堆管理,在Windows CE.NET建立的堆中建立的記憶體塊不能夠移動,多次建立記憶體塊、釋放記憶體塊會產生記憶體碎塊,這樣的話當需要分配一個大一點的連續的記憶體塊時,本來閒置記憶體塊加起來足夠用,但是這些記憶體塊是分隔的,不符合要求。像Windows 2000或98的核心會頻繁的移動分散的正使用的記憶體塊,使它們聚集在一起。這也是為什麼有時需要控制代碼而不用指標的原因。由於Windows CE.NET的堆的缺點,開發人員如果要頻繁的在堆中建立、釋放記憶體塊的話,最好自己建立一個單獨的堆,而不用預設的堆。而且我還建議最好直接在全域地址空間中(0x4200 0000到0x7FFF FFFF)分配所需地址空間。因為進程地址空間可用的實在太小了。關於堆函數我在這就不多說了,和其它Windows作業系統堆API基本一致。請參考說明文檔。
  棧也是一段連續的虛擬位址空間,和堆相比空間要小的多,它是專為函數使用的。當調用一個函數時(包括線程),核心會產生一個預設的棧,並且核心會立刻提交少量的實體記憶體(也可以禁止核心立刻提交實體記憶體)。棧的大小和CPU有關,一般為64KB,並且保留頂部2KB為了防止溢出。可以修改棧的大小,具體修改方法在講解線程的時候已經說過了,這裡就不再重複了。修改棧的大小一般時候不會發生,如果採用在編譯連結時修改大小,那麼所有棧的大小都會改變,這不太合理。實際開發中最好不要在棧中分配很大、很多的記憶體塊,如果分配的記憶體塊超過了預設棧的限制,那麼會引起訪問非法並且核心會立刻終止進程。最好在進程的堆中分配大的記憶體塊並且在函數返回前釋放,或者在建立線程時指定棧的大小。

5、記憶體對應檔
  與虛擬記憶體一樣,記憶體對應檔用來保留一個地址空間,並提交實體儲存體器。早期的記憶體對應檔並不是提交實體記憶體供調用者使用,而是提交永久儲存空間上的檔案資料。當然作業系統會為永久儲存空間保留一個讀緩衝區,這樣讀取檔案資料就快多了。記憶體對應檔的特點使它很適合於載入EXE或DLL檔案。這樣可以節省記憶體又減少了載入所需時間。還可以使用它來映射大容量的檔案,這樣就不必在讀取檔案資料前設定很大的緩衝區。另外記憶體對應檔常用於處理序間通訊,也是處理序間通訊的主要手段,其它進程之間通訊機制都是基於記憶體對應檔來實現。為了更快的在進程之間通訊,現在的記憶體對應檔也可以提交實體記憶體,這樣記憶體對應檔既可以提交實體記憶體又可以提交檔案。
  Windows CE.NET同樣支援無名和有名的記憶體對應檔。我建議在開發軟體的過程中,如果需要讀寫大容量的檔案,或者需要在不同進程內的線程之間通訊,最好採用記憶體對應檔,而且最好在全域地址空間內(0x4200 0000到0x7FFF FFFF)分配。這會使我們事半功倍。

5.1 映射資料檔案
  第一步:調用CreateFileForMapping函數。在Windows CE.NET中推薦使用這個函數替代CreateFile函數。CreateFileForMapping函數由核心執行並建立檔案,它也可以開啟由CreateFile函數建立的檔案。其參數同CreateFile相似。參數1指定檔案路徑,注意檔案路徑的格式是沒有盤符的,參數2指定訪問方式(讀或寫),參數3指定共用模式,參數4指定安全屬性(必須設定為NULL),參數5指定是建立還是開啟檔案,參數6指定檔案屬性,參數7忽略。具體參數細節參見Windows CE.NET協助。函數返回建立或者開啟的檔案的控制代碼。
  第二步:調用CreateFileMapping函數。這個函數建立一個無名的或者有名的記憶體對應檔對象。參數1為檔案控制代碼。這個值由CreateFileForMapping函數返回。參數2為安全屬性(必須設定為NULL),參數3指定要映射的檔案的保護屬性(唯讀或者讀寫),參數4和參數5共同用於指定要映射的檔案的大小。檔案的容量過大將導致32位整數也不能表示,所以這裡用64位變數表示,其中參數4為高32位元,參數5為低32位元。最後一個參數指定記憶體對應檔的名稱。這裡可以設定為NULL,表示不需要名字。
  第三步:調用MapViewOfFile函數。這個函數用於保留一段足夠的地址空間,並且將永久儲存空間上的檔案資料對應到這個地址空間。映射後這段地址空間又叫做檔案視圖,對應範圍可以是全部檔案,也可以是部分檔案。這裡需要注意的是如果檔案很大,那這個函數將在全域地址空間內分配地址空間。參數1指定記憶體對應檔對象的控制代碼,這個值由CreateFileMapping函數返回。參數2和CreateFileMapping函數中參數3很相似,都是用於限定存取權限。參數3和參數4共同用於指定映射地區的開始位置。其中參數3為高32位元,參數4為低32位元。參數5指定映射地區的大小。需要注意的是參數3和參數4指定的64位元開始位置可以不是64KB的倍數。而其它Windows作業系統就必須限制以64KB為單位。另外還要注意的是協助文檔中說不能保證一個檔案的映射視圖是連續的,並建議為了防止訪問非法,應該加入結構化異常處理機制。這個可能性我認為很小,一般對於大於2MB的虛擬位址空間的申請,核心都會在全域地址空間中分配。全域地址空間(0x4200 0000到0x7FFF FFFF)近1GB的空間應該足夠用了。畢竟Windows CE下的檔案都很小。不過在代碼中加入結構化異常處理也不是壞事。我們應該養成凡是讀寫檔案資料時都加入結構化異常處理的習慣。
  第四步:進行讀/寫操作。MapViewOfFile函數如果成功執行,那麼返回映射視圖的首地址。這時就可以把視圖當成是一個緩衝區,開始讀或寫操作了。
  第五步:執行結束工作。先調用UnmapViewOfFile函數撤銷檔案對應視圖。參數只有一個,指定視圖首地址。然後調用CloseHandle函數關閉記憶體對應檔對象,參數為控制代碼。最後再次調用CloseHandle函數,關閉開啟的檔案的控制代碼。

5.2 進程之間通訊
  進程之間有時需要通訊。系統提供的進程之間的通訊機制比如COM、剪貼簿等,在底層實現上都是利用記憶體對應檔技術。其實進程之間通訊的思路很簡單,在這裡我順便講一下。在其它Windows作業系統中,每個進程獨自佔有4GB的地址空間,高2GB是核心的地址空間,而低2GB是進程的地址空間。一個進程所能訪問的所有低2GB地址都是自己的地址空間,當訪問核心地址空間時就會受到核心的限制。這樣一個進程當然無法訪問其它進程了。為解決處理序間通訊的問題,記憶體對應檔技術被利用作為解決方案。原來記憶體對應檔只映射類似磁碟一類的儲存空間上的檔案。而為了更快速地在進程之間通訊,記憶體對應檔還可以提交實體記憶體。實現方法是通過訪問同一個記憶體對應檔對象(映射到實體記憶體),兩個進程或多個進程就能夠訪問到同一塊實體記憶體,這樣一個進程寫到實體記憶體的資料,其它進程就能夠看到了。而Windows CE雖然每個進程只佔有32MB的地址空間,而且所有進程全部處於4GB的地址空間中,但是彼此還是不能夠隨意訪問的。在Windows CE下除了使用記憶體對應檔技術外,還有一種方法也很適合使用,就是利用Object Storage Service。Object Storage Service本身使用RAM檔案系統,用普通的操作檔案的API就可以建立、讀取存在於Object Storage Service地區內的檔案。\Windows 目錄就存在於Object Storage Service地區內。我們可以利用在\Windows目錄下建立檔案來實現處理序間通訊。這種方法既實現簡單,只需調用幾個檔案API函數,又可以減少通訊時間,因為\Windows目錄存在於實體記憶體中,資料I/O當然很快了。利用Object Storage Service來實現進程之間的通訊是我自己想出來的,MSDN或其它文檔並沒有這方面的說明。需要注意的就是Object Storage Service地區的大小。另外從實現的代碼量上看也不如記憶體對應檔技術。
  下面講解如何利用記憶體對應檔實現進程之間的通訊。假設進程A和進程B需要通訊,那麼進程A需要先建立一個記憶體對應檔(之前不必調用CreateFileForMapping函數來建立檔案,因為不需要建立檔案)。這個記憶體對應檔可以是在永久儲存空間中,也可以是在記憶體中。為了減小通訊時間,最好提交實體記憶體。進程A在調用CreateFileMapping函數時,參數1指定為INVALID_HANDLE_VALUE,這表示這個記憶體對應檔對象將要把實體記憶體提交到地址空間中。最後一個參數一定要指定一個名字。進程B也同樣調用CreateFileMapping函數,而且參數相同。核心會根據名字來判斷是否已經存在一個記憶體對應檔對象,如果建立了就返回原來的對象的控制代碼。接下去就不用細說了。參照5.1去執行就可以了。要注意的是進程B調用CreateFileMapping函數後要按如下代碼檢驗函數執行結果:

HANDLE  hMap;hMap = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,1000,L"abc");if (hMap == NULL || GetLastError() != ERROR_ALREADY_EXISTS){MessageBox(L"create file mapping fail");return;}      

6、分配大的虛擬位址空間
  可以用記憶體對應檔來分配大的虛擬位址空間。也可以直接調用VirtualAlloc函數來分配。VirtualAlloc函數是最底層的分配虛擬位址空間的函數。它會在調用進程內分配合格地址空間並且自動用0初始化提交的儲存空間。傳遞一個你希望的虛擬位址空間的首地址給參數1(如果為0,那麼核心自動尋找一個合格空間),參數2為大小(單位:位元組),參數3為配置類型(提交還是保留),參數4為保護標誌(唯讀、讀寫、執行等)。函數返回分配的地址空間的首地址。在進程地址空間中每個分配的塊有三種狀態:可用、保留、提交。參數3就是指明塊的狀態。我在做實驗時發現,給參數1傳遞非0值均不成功,即使傳遞0給參數1讓核心自動尋找,得到的傳回值再次用於參數1也不成功。釋放這個虛擬位址空間調用VirtualFree函數。VirtualFree函數參數1指定首地址,參數2指定大小,參數3指定釋放類型(撤銷提交、釋放)。函數成功返回真,失敗返回假。參數3有兩個標誌,並且不能複合。當指定撤銷提交標誌(MEM_DECOMMIT)時,函數將取消這個虛擬位址空間的實體記憶體的映射,但是保留這塊虛擬位址空間。如果這個虛擬位址空間沒有提交函數也不會失敗返回。當指定釋放標誌(MEM_RELEASE)時,如果這塊虛擬位址空間含有同樣的標誌(保留或者提交)。函數將釋放這塊虛擬位址空間。如果這個虛擬位址空間有一部分提交了,其它部分沒有提交,那麼必須先調用此函數,並傳遞撤銷提交標誌,先將提交的這部分取消實體記憶體映射。然後再次調用此函數,傳遞釋放標誌。這樣整個虛擬位址空間就都能夠釋放了。關於虛擬位址空間還有其它函數,比如VirtualQuery、VirtualProtect。在這裡就不介紹了,請參見Windows CE.NET協助。

作者註:
  
《進程、線程和記憶體管理》講解的內容是我根據以前在PC機Windows作業系統中掌握的相關知識,又查看了Windows CE.NET的協助文檔和MSDN中Technical Articles和knowledge Base而得出的結論。遺憾的是Windows CE.NET的協助文檔介紹的太簡單,我只能把掌握的知識和查看到的知識相結合,另外我還做了一些實驗。我感謝瀏覽此文章的各位Windows CE下開發人員,如果你們認為有哪些地方說的不正確的,希望指出來讓我改正錯誤。讓更多的人看到的是準確無誤的文章。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.