進(jìn)程、線程、內(nèi)存管理是一個內(nèi)核最基本的
服務(wù),也是一個內(nèi)核最主要的組成部分。這幾方面的
知識是一個
軟件開發(fā)人員必須掌握的基礎(chǔ)知識。雖然一個人不懂這些知識也能編寫
簡單的程序,但這樣的程序只能算是皮毛。掌握了進(jìn)程、線程和內(nèi)存管理方面的知識,就能夠充分利用操作系統(tǒng)內(nèi)核提供的服務(wù),
提高你編寫的軟件的執(zhí)行效率、更節(jié)省
資源、更健壯。順便說一下,在Windows CE.
net下可以運(yùn)行用Visual Studio.net開發(fā)的.net平臺上運(yùn)行的軟件,但這樣的軟件是最上層的軟件,離操作系統(tǒng)內(nèi)核太遠(yuǎn)了。不但執(zhí)行效率相對較低,而且還要把.net 框架加到內(nèi)核中。所以在大多數(shù)情況下,EVC仍然是第一選擇。
圖一 CE內(nèi)核結(jié)構(gòu)
一、進(jìn)程和線程
1、概念:
Windows CE.NET是一個搶占多任務(wù)操作系統(tǒng),搶占多任務(wù)又被稱為調(diào)度。在調(diào)度過程中,內(nèi)核的調(diào)度系統(tǒng)包含一個當(dāng)前所有進(jìn)程中線程的優(yōu)先級列表,并對所有的線程按優(yōu)先級排列順序。當(dāng)中斷發(fā)生時,調(diào)度系統(tǒng)重新安排所有線程的排列順序。
一個進(jìn)程是一個正運(yùn)行的應(yīng)用程序的實例。它由兩個部分組成:一個是操作系統(tǒng)用來管理這個進(jìn)程的內(nèi)核對象。另一個是這個進(jìn)程擁有的地址
空間。這個地址空間包含應(yīng)用程序的代碼段、靜態(tài)數(shù)據(jù)段、堆、棧,非XIP(Execute In Place)DLL。從執(zhí)行角度方面看,一個進(jìn)程由一個或多個線程組成。一個線程是一個執(zhí)行單元,它
控制CPU執(zhí)行進(jìn)程中某一段代碼段。一個線程可以訪問這個進(jìn)程中所有的地址空間和資源。一個進(jìn)程最少包括一個線程來執(zhí)行代碼,這個線程又叫做主線程。
2、進(jìn)程:
Windows CE.NET最多支持32個進(jìn)程同時運(yùn)行。這是由整個系統(tǒng)分配給所有進(jìn)程的總地址空間決定的。低于Windows CE 4.0版本(也就是低于.NET的版本)的CE操作系統(tǒng),總進(jìn)程空間從0x0000 0000到0x4200 0000 ,每32MB地址空間為一個槽(Slot),共33個槽。當(dāng)一個進(jìn)程啟動時,內(nèi)核選擇一個沒有被占用的槽作為這個進(jìn)程的地址空間。其中0x0000 0000到0x01FF FFFF這個槽稱為Slot 0。每個進(jìn)程在即將得到CPU控制權(quán)時,將整個地址映射到Slot 0。這個進(jìn)程在幫助文檔中稱為當(dāng)前運(yùn)行進(jìn)程(currently running process)。分配一個槽后,內(nèi)核在這個槽內(nèi)按由低地址到高地址順序為代碼段、靜態(tài)數(shù)據(jù)段分配足夠的地址空間,然后是堆、棧,棧之后的空間為所有 DLL保留,包括XIP和非XIP DLL。注意Slot 0最底部64KB是永遠(yuǎn)保留的。從Slot 1 到 Slot32 為進(jìn)程使用。前幾個槽一般為系統(tǒng)程序使用。如filesys.exe、device.exe、gwes.exe等。
Windows CE.NET與低版本操作系統(tǒng)略有不同。這一點(diǎn)是從MSDN 的"Technical Articles"和"Knowledge Base"的文章中找到的,這的確讓我費(fèi)了一番功夫。在Windows CE.NET的幫助文件中只能找到和早期版本相同的說法,而"Technical Articles"和"Knowledge Base"中有幾篇文章清楚的說明了Windows CE.NET 和低版本操作系統(tǒng)的不同。在低版本操作系統(tǒng)中,的確如上所說分為33個槽,Slot 0用于當(dāng)前運(yùn)行進(jìn)程,共支持32個進(jìn)程同時運(yùn)行。而且所有DLL都加載到進(jìn)程的地址空間。但Windows CE.NET下 Slot 1也用于當(dāng)前進(jìn)程(Slot 1只用于加載所有XIP DLL)。那么一個進(jìn)程就不是占有32MB地址空間了,而是64MB。在講解內(nèi)存管理時我會具體講解。
創(chuàng)建一個進(jìn)程的API函數(shù)如下:
BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine,LPSECURITY_ATTRIBUTES lpProcessAttributes,LPSECURITY_ATTRIBUTES lpThreadAttributes,BOOL bInheritHandles,DWORD dwCreationFlags,LPVOID lpEnvironment,LPCTSTR lpCurrentDirectory,LPSTARTUPINFO lpStartupInfo,LPPROCESS_INFORMATION lpProcessInformation );
Windows CE.NET不支持安全性、當(dāng)前目錄、繼承性,所以這個函數(shù)很多參數(shù)都必須設(shè)為0或FALSE。具體第3、4、7、8、9設(shè)為0,第5設(shè)為FALSE。第 1參數(shù)為應(yīng)用程序名稱,這個參數(shù)不能為NULL。如果只傳遞應(yīng)用程序名稱而沒有指定路徑,那么系統(tǒng)將先搜索\Windows目錄,接著搜索OEM指定的搜索路徑。第2參數(shù)用于傳遞啟動參數(shù),必須為UNICODE碼。第6參數(shù)為創(chuàng)建標(biāo)志。可以為0(創(chuàng)建一個常規(guī)進(jìn)程)、CREATE_SUSPENDED(啟動后掛起)、DEBUG_PROCESS(用于創(chuàng)建這個進(jìn)程的父進(jìn)程調(diào)試用)、DEBUG_ONLY_THIS_PROCESS(不調(diào)試子進(jìn)程)、 CREATE_NEW_CONSOLE(控制臺進(jìn)程)。第10參數(shù)傳遞給它一個PROCESS_INFORMATION結(jié)構(gòu)變量的地址。返回進(jìn)程和主線程的句柄和ID。
終止一個進(jìn)程最好是由WinMain函數(shù)返回。在主線程中調(diào)用ExitThread函數(shù)也可以。在當(dāng)前進(jìn)程終止另一個進(jìn)程使用 TerminateProcess函數(shù)。CE下的TerminateProcess函數(shù)要比其他Windows下TerminateProcess函數(shù)功能強(qiáng)大。CE下的TerminateProcess函數(shù)在使進(jìn)程退出時,會通知每個加載的DLL并做出進(jìn)程退出時該做的所有處理工作。
3、線程:
線程除了能夠訪問進(jìn)程的資源外,每個線程還擁有自己的棧。棧的大小是可以調(diào)整的,最小為1KB或4KB(也就是一個內(nèi)存頁。內(nèi)存頁的大小取決于 CPU),一般默認(rèn)為64KB,但棧頂端永遠(yuǎn)保留2KB為防止溢出。如果要改變棧初始時大小,在EVC"Project"-"Settings"- "Link"鏈接選項"/STACK"后的參數(shù)中指定大小。其中參數(shù)1為默認(rèn)大小,參數(shù)2為一個內(nèi)存頁大小,都用十六進(jìn)制表示。如果將棧的初始值設(shè)置太小,很容易導(dǎo)致系統(tǒng)訪問非法并立即終止進(jìn)程。
線程有五中狀態(tài),分別為運(yùn)行、掛起、睡眠、阻塞、終止。當(dāng)所有線程全部處于阻塞狀態(tài)時,內(nèi)核處于空閑模式(Idle mode),這時對CPU的電力供應(yīng)將減小。
創(chuàng)建一個線程的API函數(shù)如下:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,DWORD dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId );
Windows CE.NET 不支持安全性所以參數(shù)1必須設(shè)置為0。如果參數(shù)5為STACK_SIZE_PARAM_IS_A_RESERVATION,那么參數(shù)2可以指定棧的大小,內(nèi)核將按照參數(shù)2的數(shù)值來為此線程擁有的棧保留地址空間。如果參數(shù)5不為STACK_SIZE_PARAM_IS_A_RESERVATION,那么參數(shù) 2必須設(shè)置為0。參數(shù)3為執(zhí)行路徑的首地址,也就是函數(shù)的地址。參數(shù)4用來向線程中傳遞一個參數(shù)。參數(shù)5除了上面說明外,還可以為0、 CREATE_SUSPENDED。CREATE_SUSPENDED表示這個線程在創(chuàng)建后一直處于掛起狀態(tài),直到用ResumeThread函數(shù)來恢復(fù)。最后一個參數(shù)保存函數(shù)返回的創(chuàng)建的線程ID。
退出一個線程同退出一個進(jìn)程有類似的方法。最好是由函數(shù)返回,在線程中調(diào)用 ExitThead函數(shù)也可以。在當(dāng)前線程中終止另一個線程使用TerminateThread函數(shù)。此函數(shù)在使一個線程退出時,會通知這個線程加載的所有DLL。這樣DLL就可以做結(jié)束工作了。
Windows CE.NET不像其他Windows操作系統(tǒng)將進(jìn)程分為不同的優(yōu)先級類,Windows CE.NET只將線程分為256個優(yōu)先級。0優(yōu)先級最高,255最低,0到248優(yōu)先級屬于實時性優(yōu)先級。0到247優(yōu)先級一般分配給實時性應(yīng)用程序、驅(qū)動程序、系統(tǒng)程序。249到255優(yōu)先級中,251優(yōu)先級(THREAD_PRIORITY_NORMAL)是正常優(yōu)先級。255優(yōu)先級(THREAD_PRIORITY_IDLE)為空閑優(yōu)先級。249優(yōu)先級(THREAD_PRIORITY_HIGHEST)是高優(yōu)先級。248到 255優(yōu)先級一般分配給普通應(yīng)用程序線程使用。具體分段見下表:
優(yōu)先級范圍 | 分配對象 |
0-96 | 高于驅(qū)動程序的程序 |
97-152 | 基于Windows CE的驅(qū)動程序 |
153-247 | 低于驅(qū)動程序的程序 |
248-255 | 普通的應(yīng)用程序 |
Windows CE.NET操作系統(tǒng)具有實時性,所以調(diào)度系統(tǒng)必須保證高優(yōu)先級線程先運(yùn)行,低優(yōu)先級線程在高優(yōu)先級線程終止后或者阻塞時才能得到CPU時間片。而且一旦發(fā)生中斷,內(nèi)核會暫停低優(yōu)先級線程的運(yùn)行,讓高優(yōu)先級線程繼續(xù)運(yùn)行,直到終止或者阻塞。具有相同優(yōu)先級的線程平均占有CPU時間片,當(dāng)一個線程使用完了 CPU時間片或在時間片內(nèi)阻塞、睡眠,那么其他相同優(yōu)先級的線程會占有時間片。這里提到的CPU時間片是指內(nèi)核限制線程占有CPU的時間,默認(rèn)為 100ms。OEM可以更改這個值,甚至設(shè)置為0。如果為0,當(dāng)前線程將一直占有CPU,直到更高優(yōu)先級線程要求占有CPU。這個調(diào)度算法好像是很有效、很完美,但卻存在著一種情況,當(dāng)這種情況發(fā)生時程序會死鎖。舉例來說:一個應(yīng)用程序包含兩個線程,線程1是高優(yōu)先級,線程2是低優(yōu)先級,當(dāng)線程1運(yùn)行過程中處于阻塞時,線程2得到時間片,線程2這次進(jìn)入了一個臨界區(qū),我們都知道臨界區(qū)內(nèi)的資源是不會被其它線程訪問的,當(dāng)線程2正運(yùn)行時,線程1已經(jīng)從阻塞狀態(tài)轉(zhuǎn)變?yōu)檫\(yùn)行狀態(tài),而這次線程1卻要訪問線程2的資源,這個資源卻被臨界區(qū)鎖定,那么線程1只能等待,等待線程2從臨界區(qū)中運(yùn)行結(jié)束并釋放資源的獨(dú)占權(quán)。但是線程2卻永遠(yuǎn)不會得到時間片,因為CE保證高優(yōu)先級線程會先運(yùn)行。這時程序就會處于死鎖狀態(tài)。當(dāng)然系統(tǒng)不會死鎖,因為還有更高優(yōu)先級的線程、驅(qū)動程序在運(yùn)行。對于這種情況,CE采取優(yōu)先級轉(zhuǎn)換的辦法來解決。就是當(dāng)發(fā)生這種情況時,內(nèi)核將線程2的優(yōu)先級提高到線程1的優(yōu)先級水平。這樣線程2就可以執(zhí)行完臨界區(qū)代碼了,線程1也就能夠訪問資源了。然后內(nèi)核再恢復(fù)線程2原來的優(yōu)先級。
掛起一個線程使用SuspendThread函數(shù)。參數(shù)只有一個――線程的句柄。要說明的是如果要掛起的線程正調(diào)用一個內(nèi)核功能,這時執(zhí)行此函數(shù)可能會失敗。需要多次調(diào)用此函數(shù)直到函數(shù)返回值不為 0xFFFFFFFF,說明掛起成功。恢復(fù)線程使用ResumeThread函數(shù)。參數(shù)也只有一個――線程的句柄。
關(guān)于線程本地存儲器和纖程,實際用到的時候非常少,這部分知識可以參考《Windows核心編程》。
二、同步
在多數(shù)情況下,線程之間難免要相互通信、相互協(xié)調(diào)才能完成任務(wù)。比如,當(dāng)有多個線程共同訪問同一個資源時,就必須保證一個線程正讀取這個資源數(shù)據(jù)的時候,其它線程不能夠修改它。這就需要線程之間相互通信,了解對方的行為。再有當(dāng)一個線程要準(zhǔn)備執(zhí)行下一個任務(wù)之前,它必須等待另一個線程終止才能運(yùn)行,這也需要彼此相互通信。實際開發(fā)過程中,線程間需要同步的情況非常多。Windows CE.NET給我們提供了很多的同步機(jī)制,熟練的掌握這些機(jī)制并合理運(yùn)用會使線程之間的同步更合理、更高效。進(jìn)程間的通信機(jī)制在下一篇文章中講解。
Windows CE.NET具有兩種運(yùn)行模式:用戶模式和內(nèi)核模式。并且允許一個運(yùn)行于用戶模式的應(yīng)用程序隨時切換為內(nèi)核模式,或切換回來。線程同步的有些解決辦法運(yùn)行在用戶模式,有些運(yùn)行在內(nèi)核模式?!禬indows核心編程》上說從用戶模式切換到內(nèi)核模式再切換回來至少要1000個CPU周期。我查看過CE下API 函數(shù)SetKMode的源碼,這個函數(shù)用于在兩種模式間切換,改變模式只需修改一些標(biāo)志,至于需要多少個CPU周期很難確定。但至少可以肯定來回切換是需要一定時間的。所以在選擇同步機(jī)制上應(yīng)該優(yōu)先考慮運(yùn)行在用戶模式的同步解決辦法。
1、互鎖函數(shù)
互鎖函數(shù)運(yùn)行在用戶模式。它能保證當(dāng)一個線程訪問一個變量時,其它線程無法訪問此變量,以確保變量值的唯一性。這種訪問方式被稱為原子訪問?;ユi函數(shù)及其功能見如下列表:
函數(shù) | 參數(shù)和功能 |
InterlockedIncrement | 參數(shù)為PLONG類型。此函數(shù)使一個LONG變量增1 |
InterlockedDecrement | 參數(shù)為PLONG類型。此函數(shù)使一個LONG變量減1 |
InterlockedExchangeAdd | 參數(shù)1為PLONG類型,參數(shù)2為LONG類型。此函數(shù)將參數(shù)2賦給參數(shù)1指向的值 |
InterlockedExchange | 參數(shù)1為PLONG類型,參數(shù)2為LONG類型。此函數(shù)將參數(shù)2的值賦給參數(shù)1指向的值 |
InterlockedExchangePointer | 參數(shù)為PVOID* 類型,參數(shù)2為PVOID類型。此函數(shù)功能同上。具體參見幫助 |
InterlockedCompareExchange | 參數(shù)1為PLONG類型,參數(shù)2為LONG類型,參數(shù)3為LONG類型。此函數(shù)將參數(shù)1指向的值與參數(shù)3比較,相同則把參數(shù)2的值賦給參數(shù)1指向的值。不相同則不變 |
InterlockedCompareExchangePointer | 參數(shù)1為PVOID* 類型,參數(shù)2為PVOID類型,參數(shù)3為PVOID。此函數(shù)功能同上。具體參見幫助 |
2、臨界區(qū)
臨界區(qū)對象運(yùn)行在用戶模式。它能保證在臨界區(qū)內(nèi)所有被訪問的資源不被其它線程訪問,直到當(dāng)前線程執(zhí)行完臨界區(qū)代碼。除了API外,MFC也對臨界區(qū)函數(shù)進(jìn)行了封裝。臨界區(qū)相關(guān)函數(shù):
void InitializeCriticalSection ( LPCRITICAL_SECTION );void EnterCriticalSection ( LPCRITICAL_SECTION );void LeaveCriticalSection ( LPCRITICAL_SECTION );void DeleteCriticalSection ( LPCRITICAL_SECTION );
舉例如下:
void CriticalSectionExample (void){CRITICAL_SECTION csMyCriticalSection;InitializeCriticalSection (&csMyCriticalSection); ///初始化臨界區(qū)變量__try{EnterCriticalSection (&csMyCriticalSection); ///開始保護(hù)機(jī)制///此處編寫代碼}__finally ///異常處理,無論是否異常都執(zhí)行此段代碼{LeaveCriticalSection (&csMyCriticalSection); ///撤銷保護(hù)機(jī)制}}
MFC類使用更簡單:
CCriticalSection cs;cs.Lock();///編寫代碼cs.Unlock();
使用臨界區(qū)要注意的是避免死鎖。當(dāng)有兩個線程,每個線程都有臨界區(qū),而且臨界區(qū)保護(hù)的資源有相同的時候,這時就要在編寫代碼時多加考慮。
3、事件對象
事件對象運(yùn)行在內(nèi)核模式。與用戶模式不同,內(nèi)核模式下線程利用等待函數(shù)來等待所需要的事件、信號,這個等待過程由操作系統(tǒng)內(nèi)核來完成,而線程處于睡眠狀態(tài),當(dāng)接收到信號后,內(nèi)核恢復(fù)線程的運(yùn)行。內(nèi)核模式的優(yōu)點(diǎn)是線程在等待過程中并不浪費(fèi)CPU時間,缺點(diǎn)是從用戶模式切換到內(nèi)核模式需要一定的時間,而且還要切換回來。在講解事件對象前應(yīng)該先談?wù)劦却瘮?shù)。等待函數(shù)有四個。具體參數(shù)和功能見下表:
函數(shù) | 參數(shù)和功能 |
WaitForSingleObject | 參數(shù)1為HANDLE類型,參數(shù)2為DWORD類型。此函數(shù)等待參數(shù)1標(biāo)識的事件,等待時間為參數(shù)2的值,單位ms。如果不超時,當(dāng)事件成為有信號狀態(tài)時,線程喚醒繼續(xù)運(yùn)行。 |
WaitForMultipleObjects | 參數(shù)1為DWORD類型,參數(shù)2為HANDLE * 類型,參數(shù)3為BOOL類型,參數(shù)4為DWORD類型。此函數(shù)等待參數(shù)2指向的數(shù)組中包含的所有事件。如果不超時,當(dāng)參數(shù)3為FALSE時,只要有一個事件處于有信號狀態(tài),函數(shù)就返回這個事件的索引。參數(shù)3為TRUE時,等待所有事件都處于有信號狀態(tài)時才返回。 |
MsgWaitForMultipleObjects | 參數(shù)1為DWORD類型,參數(shù)2為LPHANDLE類型,參數(shù)3為BOOL類型,參數(shù)4為DWORD類型,參數(shù)5為 DWORD類型。此函數(shù)功能上同WaitForMultipleObjects函數(shù)相似,只是多了一個喚醒掩碼。喚醒掩碼都是和消息有關(guān)的。此函數(shù)不但能夠為事件等待,還能為特定的消息等待。其實這個函數(shù)就是專為等待消息而定義的。 |
MsgWaitForMultipleObjectsEx | 參數(shù)1為DWORD類型,參數(shù)2為LPHANDLE類型,參數(shù)3為DWORD類型,參數(shù)4為DWORD類型,參數(shù)5為 DWORD類型。此函數(shù)是MsgWaitForMultipleObjects函數(shù)的擴(kuò)展。將原來函數(shù)的參數(shù)3除掉,添加參數(shù)5為標(biāo)志。標(biāo)志有兩個值:0 或MWMO_INPUTAVAILABLE。 |
如果一個線程既要執(zhí)行大量任務(wù)同時又要響應(yīng)用戶的按鍵消息,這兩個專用于等待消息的函數(shù)將非常有用。
和事件有關(guān)的函數(shù)有:
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,BOOL bManualReset,BOOL bInitialState,LPTSTR lpName);BOOL SetEvent(HANDLE hEvent );BOOL PulseEvent(HANDLE hEvent);BOOL ResetEvent(HANDLE hEvent);HANDLE OpenEvent(DWORD dwDesiredAccess,BOOL bInheritHandle,LPCTSTR lpName );
事件對象是最常用的內(nèi)核模式同步方法。它包含一個使用計數(shù)和兩個BOOL變量。其中一個BOOL變量指定這個事件對象是自動重置還是手工重置。另一個BOOL變量指定當(dāng)前事件對象處于有信號狀態(tài)還是無信號狀態(tài)。
函數(shù)CreateEvent創(chuàng)建一個事件對象,參數(shù)1必須為NULL,參數(shù)2指定是否手工重新設(shè)置事件對象的狀態(tài)。如果為FALSE,當(dāng)?shù)却瘮?shù)接到信號并返回后此事件對象被自動置為無信號狀態(tài)。這時等待此事件對象的其它線程就不會被喚醒,因為事件對象已經(jīng)被置為無信號狀態(tài)。如果參數(shù)2設(shè)置為TRUE,當(dāng)?shù)却瘮?shù)接到信號并返回后事件對象不會被自動置于無信號狀態(tài),其它等待此事件對象的線程都能夠被喚醒。用ResetEvent函數(shù)可以手工將事件對象置為無信號狀態(tài)。相反SetEvent函數(shù)將事件對象置為有信號狀態(tài)。PulseEvent函數(shù)將事件對象置為有信號狀態(tài),然后立即置為無信號狀態(tài),在實際開發(fā)中這個函數(shù)很少使用。OpenEvent函數(shù)打開已經(jīng)創(chuàng)建的事件對象,一般用于不同進(jìn)程內(nèi)的線程同步。在調(diào)用CreateEvent創(chuàng)建一個事件對象時,傳遞一個名字給參數(shù)4,這樣在其它進(jìn)程中的線程就可以調(diào)用OpenEvent函數(shù)并指定事件對象的名字,來訪問這個事件對象。
4、互斥對象
互斥對象運(yùn)行在內(nèi)核模式。它的行為特性同臨界區(qū)非常相似,在一個線程訪問某個共享資源時,它能夠保證其它線程不能訪問這個資源。不同的是,互斥對象運(yùn)行在內(nèi)核模式,從時間上比臨界區(qū)要慢。由于內(nèi)核對象具有全局性,不同的進(jìn)程都能夠訪問,這樣利用互斥對象就可以讓不同的進(jìn)程中的線程互斥訪問一個共享資源。而臨界區(qū)只能在一個進(jìn)程內(nèi)有效。
和互斥相關(guān)的函數(shù)有:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpName);BOOL ReleaseMutex(HANDLE hMutex);
互斥對象包含一個引用計數(shù),一個線程ID和一個遞歸計數(shù)。引用計數(shù)是所有內(nèi)核對象都含有的。線程ID表示哪個線程正在使用互斥資源,當(dāng)ID為0時,互斥對象發(fā)出信號。遞歸計數(shù)用于一個線程多次等待同一個互斥對象。函數(shù)CreateMutex創(chuàng)建一個互斥對象,參數(shù)1必須設(shè)置為NULL,參數(shù)2如果設(shè)置為 FALSE,表示當(dāng)前線程并不占有互斥資源,互斥對象的線程ID和遞歸計數(shù)都被設(shè)置為0,互斥對象處于有信號狀態(tài)。如果設(shè)置為TRUE,表示當(dāng)前線程將占有互斥資源,互斥對象的線程ID被設(shè)置為當(dāng)前線程ID,遞歸計數(shù)被設(shè)置為1,互斥對象處于無信號狀態(tài)。當(dāng)調(diào)用等待函數(shù)時,等待函數(shù)檢驗互斥對象的線程ID 是否為0,如果為0,說明當(dāng)前沒有線程訪問互斥資源,內(nèi)核將線程喚醒,并且將互斥對象的遞歸計數(shù)加1。當(dāng)一個線程被喚醒后,必須調(diào)用函數(shù) ReleaseMutex將互斥對象的遞歸計數(shù)減1。如果一個線程多次調(diào)用等待函數(shù),就必須以同樣的次數(shù)調(diào)用ReleaseMutex函數(shù)。與其它 Windows不同的是,和互斥相關(guān)的函數(shù)中沒有OpenMutex函數(shù)。要在不同進(jìn)程中訪問同一互斥對象,調(diào)用CreateMutex函數(shù),參數(shù)傳遞互斥對象的名稱,返回這個互斥對象的句柄。
5、信標(biāo)對象
信標(biāo)對象,也叫信號燈,用于限制資源訪問數(shù)量,他包含一個引用計數(shù),一個當(dāng)前可用資源數(shù),一個最大可用資源數(shù)。如果當(dāng)前可用資源數(shù)大于0,信標(biāo)對象處于有信號狀態(tài)。當(dāng)可用資源數(shù)等于0,信標(biāo)對象處于無信號狀態(tài)。
和信標(biāo)對象相關(guān)的函數(shù):
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,LONG lInitialCount,LONG lMaximumCount,LPCTSTR lpName);BOOL ReleaseSemaphore(HANDLE hSemaphore,LONG lReleaseCount,LPLONG lpPreviousCount);
函數(shù)CreateSemaphore的參數(shù)1為NULL,參數(shù)2為當(dāng)前可用資源初始值,參數(shù)3為最大可用資源數(shù),參數(shù)4為名字。當(dāng)參數(shù)2的值等于0時,信標(biāo)對象處于無信號狀態(tài),這時內(nèi)核將調(diào)用等待函數(shù)的線程置于睡眠狀態(tài),如果參數(shù)2的值大于0,信標(biāo)對象處于有信號狀態(tài),這時內(nèi)核將調(diào)用等待函數(shù)的線程置于運(yùn)行狀態(tài),并將信標(biāo)對象的當(dāng)前可用資源數(shù)減1。函數(shù)ReleaseSemaphore的參數(shù)1為信標(biāo)對象的句柄,參數(shù)2為要釋放的資源數(shù),參數(shù)3返回原來可用資源數(shù),調(diào)用此函數(shù)將當(dāng)前可用資源數(shù)加上參數(shù)2的值。當(dāng)一個線程訪問完可用資源后,應(yīng)該調(diào)用ReleaseSemaphore函數(shù)使當(dāng)前可用資源數(shù)遞增。要在不同進(jìn)程中訪問同一信標(biāo)對象,調(diào)用CreateSemaphore函數(shù)并傳遞信標(biāo)對象的名稱,得到已經(jīng)在其它進(jìn)程創(chuàng)建的信標(biāo)對象的句柄。CE下沒有OpenSemaphore函數(shù)。另外我還要說明一點(diǎn),等待函數(shù)默認(rèn)將信標(biāo)對象的當(dāng)前可用資源數(shù)減1,但線程可能一次使用多個資源,這就可能出現(xiàn)問題了。為避免問題出現(xiàn),應(yīng)該遵守一個線程只使用一個資源的原則。
6、消息隊列
Windows CE.NET允許一個應(yīng)用程序或驅(qū)動程序創(chuàng)建自己的消息隊列。消息隊列既可以作為在線程之間傳遞數(shù)據(jù)的工具,也可以作為線程之間同步的工具。它的優(yōu)點(diǎn)是需要很小的內(nèi)存,一般只用于點(diǎn)到點(diǎn)的通信。
和消息隊列相關(guān)的函數(shù):
HANDLE WINAPI CreateMsgQueue(LPCWSTR lpszName,LPMSGQUEUEOPTIONS lpOptions);BOOL WINAPI CloseMsgQueue(HANDLE hMsgQ);BOOL GetMsgQueueInfo(HANDLE hMsgQ,LPMSGQUEUEINFO lpInfo);HANDLE WINAPI OpenMsgQueue(HANDLE hSrcProc,HANDLE hMsgQ,LPMSGQUEUEOPTIONS lpOptions);BOOL ReadMsgQueue(HANDLE hMsgQ,LPVOID lpBuffer,DWORD cbBufferSize,LPDWORD lpNumberOfBytesRead,DWORD dwTimeout,DWORD *pdwFlags);BOOL WINAPI WriteMsgQueue(HANDLE hMsgQ,LPVOID lpBuffer,DWORD cbDataSize,DWORD dwTimeout,DWORD dwFlags);
使用CreateMsgQueue函數(shù)創(chuàng)建一個消息隊列,傳遞一個MSGQUEUEOPTIONS結(jié)構(gòu)指針。在這個結(jié)構(gòu)中設(shè)置標(biāo)志(允許隊列緩沖區(qū)動態(tài)改變大小,允許直接讀或者寫操作而不管之前是否有過寫操作或讀操作)、隊列允許的最大消息數(shù)、隊列屬性(只讀或者只寫)。使用WriteMsgQueue 函數(shù)把一個消息寫入到消息隊列中。傳遞一個消息隊列的緩沖區(qū)、消息數(shù)據(jù)的大小、寫入緩沖區(qū)的超時值、標(biāo)志。使用ReadMsgQueue函數(shù)把一個消息從消息隊列中讀出。使用CloseMsgQueue函數(shù)關(guān)閉消息隊列緩沖區(qū)。使用OpenMsgQueue函數(shù)能夠打開其它進(jìn)程中創(chuàng)建的消息隊列。另外可以用等待函數(shù)等待消息隊列的變化。當(dāng)消息隊列由沒有消息到有消息時,或由滿消息到不滿消息時喚醒調(diào)用等待函數(shù)的線程。關(guān)于消息隊列我并沒有實驗過,MSDN 上有幾個簡單的例子。
三、內(nèi)存管理
同其它Windows操作系統(tǒng)一樣,Windows CE.NET也支持32位虛擬內(nèi)存機(jī)制、按需分配內(nèi)存和內(nèi)存映射文件等。但是與其它Windows操作系統(tǒng)又有明顯的不同。畢竟Windows CE是一種嵌入式實時性的操作系統(tǒng),在內(nèi)存管理方面必須要比其它Windows操作系統(tǒng)更節(jié)約物理內(nèi)存和虛擬地址空間。在內(nèi)存管理API方面,為了便于移植程序,Windows CE和其它Windows操作系統(tǒng)函數(shù)聲明基本一致,這使一個在其它Windows下開發(fā)的程序員可以直接使用早就熟悉的API函數(shù),但是CE下內(nèi)存管理的原理開發(fā)者還是應(yīng)該熟悉的。
1、ROM和RAM
最早的基于Windows CE的民用產(chǎn)品,采用的存儲設(shè)備都是ROM + RAM ,ROM保存CE內(nèi)核文件、應(yīng)用程序,而RAM用于內(nèi)核、所有應(yīng)用程序運(yùn)行時使用,關(guān)閉電源時必須給RAM提供電力來保存系統(tǒng)配置信息、用戶產(chǎn)生的文件等。為了適應(yīng)這樣的存儲硬件,CE采用了ROM文件系統(tǒng)和RAM文件系統(tǒng)。在ROM中存放的模塊可以是壓縮的,也可以是不壓縮的,這取決于 OEM。OEM在定制內(nèi)核時可以設(shè)置是否壓縮模塊。如果是壓縮的,模塊在運(yùn)行前先解壓并全部存放到RAM中。如果是不壓縮的,就本地執(zhí)行(XIP, executed in place)。本地執(zhí)行和其它Windows操作系統(tǒng)下執(zhí)行應(yīng)用程序、DLL方式一致,也就是應(yīng)用了內(nèi)存映射文件技術(shù)。在這里我順便講一下。在啟動時應(yīng)用程序或DLL的代碼段不加載到物理內(nèi)存中,內(nèi)核只是分配虛擬地址空間給代碼段,當(dāng)執(zhí)行代碼時內(nèi)核會到實際存放在硬盤上的文件中尋找代碼并執(zhí)行。采用這樣的技術(shù)既可以節(jié)省可用內(nèi)存又可以減少加載的時間。請注意,操作系統(tǒng)首先會到為硬盤準(zhǔn)備的緩沖區(qū)里讀取代碼數(shù)據(jù),如果沒有就命令硬盤讀取應(yīng)用程序文件數(shù)據(jù)到緩沖區(qū)。所以緩沖區(qū)設(shè)置大點(diǎn)是有好處的。Windows CE的本地執(zhí)行就是采用這樣的技術(shù)來加載ROM內(nèi)的應(yīng)用程序和DLL的。所以Windows CE的DLL分為XIP DLL和非XIP DLL。這種加載方式的缺點(diǎn)就是執(zhí)行相對較慢一點(diǎn),如果用PB創(chuàng)建一個具有實時性特點(diǎn)的內(nèi)核,一定不能選用XIP技術(shù)。
到后來基于 Windows CE的產(chǎn)品開始采用FLASH、IDE等永久存儲設(shè)備時,文件系統(tǒng)又加了個FAT。內(nèi)核文件和其它應(yīng)用程序也可以存放到永久存儲設(shè)備中,內(nèi)核由加載程序解壓并加載到RAM的對象存儲區(qū)域(object store),包含在內(nèi)核中的所有系統(tǒng)應(yīng)用程序文件和DLL文件都存放到這個區(qū)域。當(dāng)執(zhí)行一個應(yīng)用程序時,內(nèi)核將這個應(yīng)用程序調(diào)用的系統(tǒng)DLL加載到 Slot 1(0x0200 0000-0x03FF FFFF)。在Windows CE.NET中Slot 1專用于XIP DLL使用。
RAM文件系統(tǒng)專用于對象存儲。在以前的文章中曾經(jīng)講過,它和ROM文件系統(tǒng)是Windows CE默認(rèn)的文件系統(tǒng)。Windows CE啟動后把RAM分為對象存儲區(qū)域(object store)和應(yīng)用程序內(nèi)存區(qū)域(program memory)。對象存儲區(qū)域采用RAM文件系統(tǒng)來保存文件,一般用于保存內(nèi)核解開的所有文件。應(yīng)用程序內(nèi)存區(qū)域留給所有應(yīng)用程序運(yùn)行時使用。在 Windows CE下"控制面板"-"系統(tǒng)"-"內(nèi)存"中,可以調(diào)節(jié)這兩個存儲區(qū)域的比例,滑塊向左,則釋放對象存儲區(qū)域的一些內(nèi)存并將這些內(nèi)存劃到應(yīng)用程序內(nèi)存區(qū)域中?;瑝K向右則相反。
2、內(nèi)存結(jié)構(gòu)
Windows CE.NET只能管理512MB的物理內(nèi)存和4GB大小的虛擬地址空間。不同的CPU內(nèi)存管理方法也不同。對于MIPS和SHX系列CPU來說,物理地址映射是由CPU完成的, CE內(nèi)核可以直接訪問512MB的物理內(nèi)存。對于x86系列和ARM系列的CPU來說,在內(nèi)核啟動過程中它會將現(xiàn)有物理內(nèi)存地址全部映射到0x8000 0000以上的虛擬地址空間中供內(nèi)核以后使用。OEM可以通過OEMAddressTable來詳細(xì)定義虛擬地址和物理地址的映射關(guān)系。 OEMAddressTable本身并不是一個文件,它只是存在于其它文件中描述虛擬地址和實際物理地址的映射關(guān)系的數(shù)據(jù)。比如文件oem init.asm中包含一段代碼:dd 80000000h, 0, 04000000h 。它表示將整個物理地址(0x0400 0000=64MB)共64MB映射到虛擬地址從0x8000 0000到0x8400 0000中。關(guān)于OEMAddressTable我將在以后關(guān)于PB的文章中講述。
整個4GB虛擬地址空間主要劃分為兩部分,從0x8000 0000以上為內(nèi)核使用部分,0x8000 0000以下為應(yīng)用程序使用部分。詳細(xì)見下表:
地址范圍 | 用途 |
0x0000 0000到0x41FF FFFF | 由所有應(yīng)用程序使用。共33個槽,每個槽占32MB。槽0(Slot 0)由當(dāng)前占有CPU的進(jìn)程使用。槽1由XIP DLL使用。其它槽用于進(jìn)程使用,每個進(jìn)程占用一個槽。 |
0x4200 0000到0x7FFF FFFF | 由所有應(yīng)用程序共享的區(qū)域。32MB地址空間有時不能夠滿足一些進(jìn)程的需求。那么進(jìn)程可以使用這個范圍的地址空間。在這個區(qū)域里應(yīng)用程序可以建堆、創(chuàng)建內(nèi)存映射文件、分配大的地址空間等。 |
0xA000 0000到0xBFFF FFFF | 在這個范圍內(nèi)核重復(fù)定義0x8000 0000到0x9FFF FFFF之間定義的物理地址映射空間。區(qū)別是在這范圍映射的虛擬地址空間不能夠用于緩沖。 我舉例來說明:假設(shè)一個產(chǎn)品有64MB物理內(nèi)存。如上文所述定義好OEMAddressTable后。內(nèi)核啟動后一個物理地址映射空間范圍在 0x8000 0000到0x8400 0000,那么內(nèi)核會從0xA000 0000到0xA400 0000定義一個同樣范圍的地址空間,這個地址空間和0x8000 0000到0x8400 0000映射到相同的物理地址。但這個虛擬地址空間不能夠用于緩沖。 |
0xC000 0000到0xC1FF FFFF | 系統(tǒng)保留空間 |
0xC200 0000到0xC3FF FFFF | 內(nèi)核程序nk.exe使用的地址空間。 |
0xC400 0000到0xDFFF FFFF | 這個范圍為用戶定義的靜態(tài)虛擬地址空間,但這個地址空間只能用于非緩沖使用。 利用 OEMAddressTable定義物理地址映射空間后,每次內(nèi)核啟動時這個范圍都不改變了,除非產(chǎn)品包含的物理內(nèi)存容量發(fā)生變化。假如增加到128MB 物理內(nèi)存,那么物理地址映射空間也向后擴(kuò)大了一倍。Windows CE.NET也允許用戶創(chuàng)建靜態(tài)的物理地址映射空間。用戶可以調(diào)用CreateStaticMapping函數(shù)或者 NKCreateStaticMapping函數(shù)來映射某一段物理地址到0xC400 0000和0xE000 0000之間的某一個范圍。需要注意的是用這個函數(shù)創(chuàng)建的靜態(tài)虛擬地址只能夠由內(nèi)核訪問,而且不能用于緩沖。 |
0xE000 0000到0xFFFF FFFF | 內(nèi)核使用的虛擬地址。當(dāng)內(nèi)核需要大的虛擬地址空間時,會在這個范圍內(nèi)分配。 |

圖1 Windows CE.NET內(nèi)存結(jié)構(gòu)
3、進(jìn)程地址空間結(jié)構(gòu)
進(jìn)程地址空間結(jié)構(gòu)如圖2所示。這個圖源至MSDN。Windows CE.NET同以前版本的Windows CE操作系統(tǒng)在進(jìn)程地址空間上有所不同,以前的Windows CE把XIP DLL也加載到進(jìn)程的32MB地址空間中,而Windows CE.NET把XIP DLL單獨(dú)加載到Slot 1中,這樣對于每個進(jìn)程來說,它總的地址空間就大了一倍,也就是64MB。這個問題我在講解進(jìn)程的時候提到過。
當(dāng)一個應(yīng)用程序啟動時,內(nèi)核為這個程序選擇一個空閑的槽(Slot),并且加載所有的代碼、資源,并分配堆棧,加載DLL等。當(dāng)這個進(jìn)程得到CPU使用權(quán)時,它的整個地址空間被內(nèi)核映射到Slot 0,也就是當(dāng)前進(jìn)程使用的地址空間,然后開始運(yùn)行。圖中給出的地址實際上是經(jīng)過映射到Slot 0之后的結(jié)構(gòu)。從圖中可以看出,進(jìn)程首先加載代碼段,因為每個進(jìn)程最低部64KB作為保留區(qū)域,所以代碼段從0x0001 0000開始,內(nèi)核為代碼段分配足夠的虛擬地址空間后,接著分配空間為只讀數(shù)據(jù)和可讀/可寫數(shù)據(jù),接著分配空間為資源數(shù)據(jù),之后分配空間為默認(rèn)堆和棧。非 XIP DLL從進(jìn)程最高地址向下開始加載。非XIP DLL的加載按如下規(guī)則:內(nèi)核先檢查要加載的DLL是否被其它進(jìn)程加載過,如果加載過,就做一個地址的重定位。這樣就避免了整個系統(tǒng)內(nèi)多次加載相同 DLL。如果沒有加載過,就按照從槽的高地址到槽的低地址的順序查找空閑的地址空間。然后分配足夠的地址空間用于加載DLL。因為每個進(jìn)程在執(zhí)行前都要映射到Slot 0,而且進(jìn)程使用的所有DLL可能來自不同的槽(Slot),為避免所有使用的DLL在映射到Slot 0中出現(xiàn)地址空間沖突的現(xiàn)象,內(nèi)核的加載器(Loader)在加載DLL時會查找所有槽中加載的DLL的地址,保證在映射到Slot 0時不會發(fā)生地址沖突現(xiàn)象。假如系統(tǒng)內(nèi)有兩個進(jìn)程,進(jìn)程A只加載了DLL A,進(jìn)程B需要加載DLL A和DLL B,那么進(jìn)程B會留出DLL A的地址空間,然后加載DLL B,也就是說進(jìn)程B映射到Slot 0時,DLL A的地址空間和DLL B的地址空間是相鄰的,不會發(fā)生沖突。好在Windows CE下DLL都很小,而且一個應(yīng)用程序使用的DLL多數(shù)是系統(tǒng)的DLL(存在于Slot 1)。所以目前來看進(jìn)程的地址空間還夠用。

圖2 進(jìn)程地址空間結(jié)構(gòu)
4、堆和棧
堆是一段連續(xù)的較大的虛擬地址空間。應(yīng)用程序在堆中可以動態(tài)地分配、釋放所需大小的內(nèi)存塊。利用堆的優(yōu)點(diǎn)是在一定范圍內(nèi)減小了內(nèi)存碎塊。而且開發(fā)者分配內(nèi)存塊前不必去了解CPU的類型。因為不同的CPU分頁大小不相同,每個內(nèi)存頁可能是1KB、4KB或更多。在堆內(nèi)分配內(nèi)存塊可以是任意大小的,而直接分配內(nèi)存就必須以內(nèi)存頁為單位。當(dāng)一個應(yīng)用程序啟動時,內(nèi)核在進(jìn)程所在的地址空間中為進(jìn)程分配一個默認(rèn)192KB大小的虛擬地址空間,但是并不立刻提交物理內(nèi)存。如果在運(yùn)行當(dāng)中192KB不能滿足需求,那么內(nèi)核會在進(jìn)程地址空間中重新查找一個足夠大小的空閑的地址空間,然后復(fù)制原來堆的數(shù)據(jù),最后釋放原來的堆所占的地址空間。這是因為默認(rèn)的堆的高地址處還有棧,所以必須重新分配一個。Windows CE.NET的堆有明顯的缺點(diǎn),不同于其它Windows操作系統(tǒng)下的堆管理,在Windows CE.NET創(chuàng)建的堆中創(chuàng)建的內(nèi)存塊不能夠移動,多次創(chuàng)建內(nèi)存塊、釋放內(nèi)存塊會產(chǎn)生內(nèi)存碎塊,這樣的話當(dāng)需要分配一個大一點(diǎn)的連續(xù)的內(nèi)存塊時,本來空閑的內(nèi)存塊加起來足夠用,但是這些內(nèi)存塊是分隔的,不符合要求。像Windows 2000或98的內(nèi)核會頻繁的移動分散的正使用的內(nèi)存塊,使它們聚集在一起。這也是為什么有時需要句柄而不用指針的原因。由于Windows CE.NET的堆的缺點(diǎn),開發(fā)者如果要頻繁的在堆中創(chuàng)建、釋放內(nèi)存塊的話,最好自己創(chuàng)建一個單獨(dú)的堆,而不用默認(rèn)的堆。而且我還建議最好直接在全局地址空間中(0x4200 0000到0x7FFF FFFF)分配所需地址空間。因為進(jìn)程地址空間可用的實在太小了。關(guān)于堆函數(shù)我在這就不多說了,和其它Windows操作系統(tǒng)堆API基本一致。請參考幫助文檔。
棧也是一段連續(xù)的虛擬地址空間,和堆相比空間要小的多,它是專為函數(shù)使用的。當(dāng)調(diào)用一個函數(shù)時(包括線程),內(nèi)核會產(chǎn)生一個默認(rèn)的棧,并且內(nèi)核會立刻提交少量的物理內(nèi)存(也可以禁止內(nèi)核立刻提交物理內(nèi)存)。棧的大小和CPU有關(guān),一般為64KB,并且保留頂部2KB為了防止溢出。可以修改棧的大小,具體修改方法在講解線程的時候已經(jīng)說過了,這里就不再重復(fù)了。修改棧的大小一般時候不會發(fā)生,如果采用在編譯鏈接時修改大小,那么所有棧的大小都會改變,這不太合理。實際開發(fā)中最好不要在棧中分配很大、很多的內(nèi)存塊,如果分配的內(nèi)存塊超過了默認(rèn)棧的限制,那么會引起訪問非法并且內(nèi)核會立刻終止進(jìn)程。最好在進(jìn)程的堆中分配大的內(nèi)存塊并且在函數(shù)返回前釋放,或者在創(chuàng)建線程時指定棧的大小。
5、內(nèi)存映射文件
與虛擬內(nèi)存一樣,內(nèi)存映射文件用來保留一個地址空間,并提交物理存儲器。早期的內(nèi)存映射文件并不是提交物理內(nèi)存供調(diào)用者使用,而是提交永久存儲器上的文件數(shù)據(jù)。當(dāng)然操作系統(tǒng)會為永久存儲器保留一個讀緩沖區(qū),這樣讀取文件數(shù)據(jù)就快多了。內(nèi)存映射文件的特點(diǎn)使它很適合于加載EXE或DLL文件。這樣可以節(jié)省內(nèi)存又減少了加載所需時間。還可以使用它來映射大容量的文件,這樣就不必在讀取文件數(shù)據(jù)前設(shè)置很大的緩沖區(qū)。另外內(nèi)存映射文件常用于進(jìn)程間通信,也是進(jìn)程間通信的主要手段,其它進(jìn)程之間通信機(jī)制都是基于內(nèi)存映射文件來實現(xiàn)。為了更快的在進(jìn)程之間通信,現(xiàn)在的內(nèi)存映射文件也可以提交物理內(nèi)存,這樣內(nèi)存映射文件既可以提交物理內(nèi)存又可以提交文件。
Windows CE.NET同樣支持無名和有名的內(nèi)存映射文件。我建議在開發(fā)軟件的過程中,如果需要讀寫大容量的文件,或者需要在不同進(jìn)程內(nèi)的線程之間通信,最好采用內(nèi)存映射文件,而且最好在全局地址空間內(nèi)(0x4200 0000到0x7FFF FFFF)分配。這會使我們事半功倍。
5.1 映射數(shù)據(jù)文件
第一步:調(diào)用CreateFileForMapping函數(shù)。在Windows CE.NET中推薦使用這個函數(shù)替代CreateFile函數(shù)。CreateFileForMapping函數(shù)由內(nèi)核執(zhí)行并創(chuàng)建文件,它也可以打開由 CreateFile函數(shù)創(chuàng)建的文件。其參數(shù)同CreateFile相似。參數(shù)1指定文件路徑,注意文件路徑的格式是沒有盤符的,參數(shù)2指定訪問方式(讀或?qū)懀?,參?shù)3指定共享模式,參數(shù)4指定安全屬性(必須設(shè)置為NULL),參數(shù)5指定是創(chuàng)建還是打開文件,參數(shù)6指定文件屬性,參數(shù)7忽略。具體參數(shù)細(xì)節(jié)參見Windows CE.NET幫助。函數(shù)返回創(chuàng)建或者打開的文件的句柄。
第二步:調(diào)用CreateFileMapping函數(shù)。這個函數(shù)創(chuàng)建一個無名的或者有名的內(nèi)存映射文件對象。參數(shù)1為文件句柄。這個值由CreateFileForMapping函數(shù)返回。參數(shù)2為安全屬性(必須設(shè)置為NULL),參數(shù)3指定要映射的文件的保護(hù)屬性(只讀或者讀寫),參數(shù)4和參數(shù)5共同用于指定要映射的文件的大小。文件的容量過大將導(dǎo)致32位整數(shù)也不能表示,所以這里用64位變量表示,其中參數(shù)4為高32位數(shù),參數(shù)5為低32位數(shù)。最后一個參數(shù)指定內(nèi)存映射文件的名稱。這里可以設(shè)置為NULL,表示不需要名字。
第三步:調(diào)用MapViewOfFile函數(shù)。這個函數(shù)用于保留一段足夠的地址空間,并且將永久存儲器上的文件數(shù)據(jù)映射到這個地址空間。映射后這段地址空間又叫做文件視圖,映射范圍可以是全部文件,也可以是部分文件。這里需要注意的是如果文件很大,那這個函數(shù)將在全局地址空間內(nèi)分配地址空間。參數(shù)1指定內(nèi)存映射文件對象的句柄,這個值由CreateFileMapping函數(shù)返回。參數(shù)2和CreateFileMapping函數(shù)中參數(shù)3很相似,都是用于限定訪問權(quán)限。參數(shù)3和參數(shù)4共同用于指定映射區(qū)域的開始位置。其中參數(shù)3為高32位數(shù),參數(shù)4為低32位數(shù)。參數(shù)5指定映射區(qū)域的大小。需要注意的是參數(shù)3和參數(shù)4指定的64位數(shù)開始位置可以不是64KB的倍數(shù)。而其它Windows操作系統(tǒng)就必須限制以64KB為單位。另外還要注意的是幫助文檔中說不能保證一個文件的映射視圖是連續(xù)的,并建議為了防止訪問非法,應(yīng)該加入結(jié)構(gòu)化異常處理機(jī)制。這個可能性我認(rèn)為很小,一般對于大于 2MB的虛擬地址空間的申請,內(nèi)核都會在全局地址空間中分配。全局地址空間(0x4200 0000到0x7FFF FFFF)近1GB的空間應(yīng)該足夠用了。畢竟Windows CE下的文件都很小。不過在代碼中加入結(jié)構(gòu)化異常處理也不是壞事。我們應(yīng)該養(yǎng)成凡是讀寫文件數(shù)據(jù)時都加入結(jié)構(gòu)化異常處理的習(xí)慣。
第四步:進(jìn)行讀/寫操作。MapViewOfFile函數(shù)如果成功執(zhí)行,那么返回映射視圖的首地址。這時就可以把視圖當(dāng)成是一個緩沖區(qū),開始讀或?qū)懖僮髁恕?br> 第五步:執(zhí)行結(jié)束工作。先調(diào)用UnmapViewOfFile函數(shù)撤銷文件映射視圖。參數(shù)只有一個,指定視圖首地址。然后調(diào)用CloseHandle函數(shù)關(guān)閉內(nèi)存映射文件對象,參數(shù)為句柄。最后再次調(diào)用CloseHandle函數(shù),關(guān)閉打開的文件的句柄。
5.2 進(jìn)程之間通信
進(jìn)程之間有時需要通信。系統(tǒng)提供的進(jìn)程之間的通信機(jī)制比如COM、剪貼板等,在底層實現(xiàn)上都是利用內(nèi)存映射文件技術(shù)。其實進(jìn)程之間通信的思路很簡單,在這里我順便講一下。在其它Windows操作系統(tǒng)中,每個進(jìn)程獨(dú)自占有4GB的地址空間,高2GB是內(nèi)核的地址空間,而低2GB是進(jìn)程的地址空間。一個進(jìn)程所能訪問的所有低2GB地址都是自己的地址空間,當(dāng)訪問內(nèi)核地址空間時就會受到內(nèi)核的限制。這樣一個進(jìn)程當(dāng)然無法訪問其它進(jìn)程了。為解決進(jìn)程間通信的問題,內(nèi)存映射文件技術(shù)被利用作為解決方案。原來內(nèi)存映射文件只映射類似磁盤一類的存儲器上的文件。而為了更快速地在進(jìn)程之間通信,內(nèi)存映射文件還可以提交物理內(nèi)存。實現(xiàn)方法是通過訪問同一個內(nèi)存映射文件對象(映射到物理內(nèi)存),兩個進(jìn)程或多個進(jìn)程就能夠訪問到同一塊物理內(nèi)存,這樣一個進(jìn)程寫到物理內(nèi)存的數(shù)據(jù),其它進(jìn)程就能夠看到了。而Windows CE雖然每個進(jìn)程只占有32MB的地址空間,而且所有進(jìn)程全部處于4GB的地址空間中,但是彼此還是不能夠隨意訪問的。在Windows CE下除了使用內(nèi)存映射文件技術(shù)外,還有一種方法也很適合使用,就是利用對象存儲。對象存儲本身使用RAM文件系統(tǒng),用普通的操作文件的API就可以創(chuàng)建、讀取存在于對象存儲區(qū)域內(nèi)的文件。\Windows 目錄就存在于對象存儲區(qū)域內(nèi)。我們可以利用在\Windows目錄下創(chuàng)建文件來實現(xiàn)進(jìn)程間通信。這種方法既實現(xiàn)簡單,只需調(diào)用幾個文件API函數(shù),又可以減少通信時間,因為\Windows目錄存在于物理內(nèi)存中,數(shù)據(jù)I/O當(dāng)然很快了。利用對象存儲來實現(xiàn)進(jìn)程之間的通信是我自己想出來的,MSDN或其它文檔并沒有這方面的說明。需要注意的就是對象存儲區(qū)域的大小。另外從實現(xiàn)的代碼量上看也不如內(nèi)存映射文件技術(shù)。
下面講解如何利用內(nèi)存映射文件實現(xiàn)進(jìn)程之間的通信。假設(shè)進(jìn)程A和進(jìn)程B需要通信,那么進(jìn)程A需要先創(chuàng)建一個內(nèi)存映射文件(之前不必調(diào)用CreateFileForMapping函數(shù)來創(chuàng)建文件,因為不需要創(chuàng)建文件)。這個內(nèi)存映射文件可以是在永久存儲器中,也可以是在內(nèi)存中。為了減小通信時間,最好提交物理內(nèi)存。進(jìn)程A在調(diào)用 CreateFileMapping函數(shù)時,參數(shù)1指定為INVALID_HANDLE_VALUE,這表示這個內(nèi)存映射文件對象將要把物理內(nèi)存提交到地址空間中。最后一個參數(shù)一定要指定一個名字。進(jìn)程B也同樣調(diào)用CreateFileMapping函數(shù),而且參數(shù)相同。內(nèi)核會根據(jù)名字來判斷是否已經(jīng)存在一個內(nèi)存映射文件對象,如果創(chuàng)建了就返回原來的對象的句柄。接下去就不用細(xì)說了。參照5.1去執(zhí)行就可以了。要注意的是進(jìn)程B調(diào)用 CreateFileMapping函數(shù)后要按如下代碼檢驗函數(shù)執(zhí)行結(jié)果:
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、分配大的虛擬地址空間
可以用內(nèi)存映射文件來分配大的虛擬地址空間。也可以直接調(diào)用VirtualAlloc函數(shù)來分配。VirtualAlloc函數(shù)是最底層的分配虛擬地址空間的函數(shù)。它會在調(diào)用進(jìn)程內(nèi)分配符合條件的地址空間并且自動用0初始化提交的存儲器。傳遞一個你希望的虛擬地址空間的首地址給參數(shù)1(如果為0,那么內(nèi)核自動查找一個符合條件的空間),參數(shù)2為大小(單位:字節(jié)),參數(shù)3為分配類型(提交還是保留),參數(shù)4為保護(hù)標(biāo)志(只讀、讀寫、執(zhí)行等)。函數(shù)返回分配的地址空間的首地址。在進(jìn)程地址空間中每個分配的塊有三種狀態(tài):可用、保留、提交。參數(shù)3就是指明塊的狀態(tài)。我在做實驗時發(fā)現(xiàn),給參數(shù)1傳遞非0值均不成功,即使傳遞0給參數(shù)1讓內(nèi)核自動查找,得到的返回值再次用于參數(shù)1也不成功。釋放這個虛擬地址空間調(diào)用VirtualFree函數(shù)。 VirtualFree函數(shù)參數(shù)1指定首地址,參數(shù)2指定大小,參數(shù)3指定釋放類型(撤銷提交、釋放)。函數(shù)成功返回真,失敗返回假。參數(shù)3有兩個標(biāo)志,并且不能復(fù)合。當(dāng)指定撤銷提交標(biāo)志(MEM_DECOMMIT)時,函數(shù)將取消這個虛擬地址空間的物理內(nèi)存的映射,但是保留這塊虛擬地址空間。如果這個虛擬地址空間沒有提交函數(shù)也不會失敗返回。當(dāng)指定釋放標(biāo)志(MEM_RELEASE)時,如果這塊虛擬地址空間含有同樣的標(biāo)志(保留或者提交)。函數(shù)將釋放這塊虛擬地址空間。如果這個虛擬地址空間有一部分提交了,其它部分沒有提交,那么必須先調(diào)用此函數(shù),并傳遞撤銷提交標(biāo)志,先將提交的這部分取消物理內(nèi)存映射。然后再次調(diào)用此函數(shù),傳遞釋放標(biāo)志。這樣整個虛擬地址空間就都能夠釋放了。關(guān)于虛擬地址空間還有其它函數(shù),比如VirtualQuery、 VirtualProtect。在這里就不介紹了,請參見Windows CE.NET幫助。
作者注:
《進(jìn)程、線程和內(nèi)存管理》講解的內(nèi)容是我根據(jù)以前在PC機(jī)Windows操作系統(tǒng)中掌握的相關(guān)知識,又查看了Windows CE.NET的幫助文檔和MSDN中Technical Articles和knowledge Base而得出的結(jié)論。遺憾的是Windows CE.NET的幫助文檔介紹的太簡單,我只能把掌握的知識和查看到的知識相結(jié)合,另外我還做了一些實驗。我感謝瀏覽此文章的各位Windows CE下開發(fā)者,如果你們認(rèn)為有哪些地方說的不正確的,希望指出來讓我改正錯誤。讓更多的人看到的是準(zhǔn)確無誤的文章。