Windows® CE 系統中的同步機制
2006-1-17
摘要
Windows ® CE 是微軟系列嵌入式平臺所採用的操作系統內核。本文討論了 WinCE 進程 / 線程之間的同步機制,給出了它們的典型應用場景。這些同步機制包括臨界區、互斥體、信號量、事件、互鎖函數和消息隊列等。
關鍵字:
同步,臨界區,互斥體,信號量,事件,消息隊列
Keywords
: Synchronization, Critical Section, Mutex, Semaphore, Event, Message Queue
適用範圍 :本文全部內容適用於 WinCE.net 4.0 及以上版本,以及以這樣的 WinCE 做內核的 Windows ® Mobile 系列平臺。各個章節中對每種同步機制的適用版本還有說明。
知識準備 :操作系統, UML , etc.
目錄
一、 WinCE 進程 / 線程模型概覽
WinCE 操作系統實現了進程 / 線程兩級管理模型。連同內核進程和系統進程,以及應用進程一起, WinCE 共支持 32 個進程。進程還可以有自己的線程,進程默認有一個主線程,線程分爲 256 個優先級別, WinCE 調度程序按照線程優先級高低來調度。進程是 WinCE 中最小資源分配的單元,線程是 WinCE 的最小調度單元。
本文講述的同步機制有些只適用於線程間同步,有些既能用於線程間同步又能用於進程間同步,下面討論到某一種機制的時候,再具體詳述其適用場景。
二、臨界區( Critical Section )
(本節內容適用於 WinCE 1.0 及以上版本 )
WinCE 實現了操作系統理論裏的臨界區管理。臨界區內含有對臨街資源的訪問。通過對臨界區進行有效管理,使得某一時刻最多隻能有一個線程進入臨界區,實現對臨界資源的保護。
考慮下面用臨界區實現兩個線程對臨界資源互斥訪問的情形。 The 1st Thread 和 The 2nd Thread 都要調用 Func_CriticalSection() 函數,而 Func_CriticalSection() 內部會對某一臨界資源進行操作,爲了保護這一臨界資源,我們用一個 WinCE 的 CriticalSection 來實現。
圖一是該解決方案的一個場景。 The 1st Thread 和 The 2nd Thread 進入臨界區之前已經創建( new )並初始化( InitializeCriticalSection() )了一個臨界區。試圖進入該臨界區的線程首先必須獲得進入該臨界區(通過 EnterCriticalSection() / TryEnterCriticalSection() )的資格 ,如果臨界區內沒有線程,它就能進入,否則必須被掛起等待。進入臨界區的線程可以對臨界資源進行操作( OpOnSharedResources() )。操作完成之後退出臨界區( LeaveCriticalSection () ),以允許其它線程進入。圖一中第一個線程進入臨界區還未退出之前,第二個線程因執行 EnterCriticalSection() 而 一直在被掛起等待,第一個線程退出臨界區之後,第二個線程從等待中被喚醒,按照相應的調度機制重新競爭獲得 CPU ,從而繼續執行,完成臨界區內的操作。
圖一、應用臨界區( CriticalSection )實現同步
利用臨界區可以實現對臨界資源的互斥操作, WinCE 的臨界區應用在同一進程內,亦即實現的是同一進程內的線程間同步,不能應用在進程之間。
三、互斥體( Mutex )
(本節內容適用於 WinCE 1.01 及以上版本 )
互斥體( Mutex )顧名思義就是實現對共享資源實現互斥訪問的。 WinCE 中的互斥體的使用規則如下(按線程之間的同步爲例):
◇
互斥體可以是匿名互斥體也可以是命名互斥體;
◇
線程創建互斥體的時候可以指定創建完畢它是否就立即擁有該互斥體;
◇
某一時刻最多隻有一個線程擁有給定的互斥體;
◇
擁有互斥體的線程可多次獲得該互斥體;
◇
線程可用
CreateMutex
或
wait函數
來獲得互斥體。
看下面應用互斥體的情景。 Thread1 創建並擁有了一個互斥體 g_hMutex[ 序列 1&2] 。互斥體 g_hMutex 是定義的全局量, thread2 可訪問到, Thread2 用 WaitForSingleObject() 試圖獲得該互斥體,因爲此時 g_hMutex 是被 Thread1 擁有的,所以 Thread2 被掛起 [ 序列 3] 。 Thread1 執行了一些操作之後,又用 wait 函數試圖再次獲得了該互斥體,因爲此時 g_hMutex 的擁有者還是 Thread1 ,所以 Thread1 立即再次獲得了該互斥體 [ 序列 4-6] 。 Thread1 對互斥體 g_hMutex 保護的共享資源操作完畢,釋放該互斥體 [ 序列 7] ,但是因爲 Thread1 兩次獲得了 g_hMutex ,所以 g_hMutex 的擁有權並沒有交出。等到 Thread1 再次釋放互斥體 g_hMutex[ 序列 8] 之後, Thread1 才失去了 g_hMutex 的擁有權, Thread2 可競爭 g_hMutex 的擁有權,如能成功擁有,就可從等待狀態被喚醒,完成對共享資源的訪問操作。
圖二、應用互斥體( Mutex )實現同步
不 知道從上面的描述,讀者有又沒有看出互斥體與臨界區之間的區別。使用上,它們都實現的對共享資源的互斥訪問,但是臨界區是多個線程對同一段程序的執行,這 段程序會訪問到臨界資源,所以它們是同一個進程內的多個線程;而互斥體的應用情景是在線程之間獨立執行,可以不是程序上的重疊,只是一個線程執行到共享資 源的時候,有可能別的線程也要訪問該共享資源,所以要用互斥體來保護該共享資源。
由於互斥體上述的應用範圍,它不但能應用在同一進程內的線程之間,也能應用在進程之間。進程之間可以通過命名互斥體來實現。一個進程通過爲 CreateMutex() 指定一個名字做參數來獲得已經存在的互斥體的句柄,處理過程如下面程序所示。
HANDLE
hMutex;
hMutex = CreateMutex (
NULL,
//
FALSE,
// Mutex object NOT initially owned
TEXT("NameOfMutexObject")); // Muetx Name
if (NULL == hMutex)
{
// Something wrong, deal with it here.
}
else
{
if ( ERROR_ALREADY_EXISTS == GetLastError () )
{
// CreateMutex() opened existing mutex."
// ...
}
else
{
// CreateMutex() created new mutex."
// ...
}
}
進程獲得已經存在的互斥體的句柄之後,就可以如線程之間同步規則那樣來實現進程之間的互斥體使用。
四、信號量( Semaphore )
(本節內容適用於 WinCE 3.0 及以上版本 )
信號量實體有一個數值指示當前該信號量使用情況,當前值的大小處於零和最大值之間。用下列操作原語實現信號量的同步操作(用線程間同步來說明):
◇
P
(
S, num
)
:如果信號量當前值減去
num
大於零,執行該操作的線程獲得信號量,可繼續執行,同時信號量的當前值減小
num
;否則訪問線程被掛起等待
◇
V
(
S, num
)
:信號量的當前值增加
num
(增加之後仍不大於最大值),如果有等待該信號量的線程被掛起,喚醒等待線程並按照相應的調度機制參與調度。
信號量一般用來控制某類共享資源,最大值標識該類資源的數目,執行 P 操作是申請一定數目這類資源, V 操作是釋放一定數目的這類資源。在 WinCE 的信號量實現中,並未實現 OpenSemaphore , P 操作是用 wait函數 來實現的,而 V 操作由 ReleaseSemaphore 來實現。
看下面用信號量來控制數量爲 2 的某類共享資源的使用情景。
圖三、用信號量( Semaphore )實現同步
Thread1 創建一個控制 2 個共享資源的信號量 [ 序列 1&2] ,並且自己用 WaitForSingleObject() 來申請一個資源,因爲當前可用的這類資源有 2 個,所以它就獲得了其中的一個 [ 序列 3&4] 。同樣地, Thread2 獲得了另外一個資源 [ 序列 5&6] 。但是當 Thread3 也申請這類資源的時候,因爲此時已經沒有這類資源,信號量的值爲零,它就被掛起 [ 序列 7] 。擁有這類資源的線程釋放掉一個資源 [ 序列 8&9] ,並且滿足能滿足 Thread3 申請資源數目的要求, Thread3 競爭獲得了該資源 [ 序列 10] 。
信號量是實現同步的基本方法,在幾乎所有的多任務操作系統裏面都做了信號量的實現,其它一些同步機制其實可以通過信號量來實現。如果把信號量的最大值和初始值均設置爲 1 ,那麼它就可實現互斥體 ,即保證對共享資源互斥訪問的保護。如果把信號量的初始值設置爲 0 ,等待別的線程 ReleaseSemaphore 來喚醒它,那麼它就可實現事件( Event ) 機制。
信號量機制可以用在同一進程內的線程之間同步,也可以用在進程之間的同步。進程間同步的實現方法如同互斥體的此類實現。
五、事件( Event )
(本節內容適用於 WinCE 1.0 及以上版本 )
WinCE 系統中廣泛用到事件( Event )機制來實現線程之間的協調工作,具體表現在:
◇
通知一個線程什麼時候去執行它的特定的任務
◇
標識事件的發生
WinCE 中的線程操作原語有 CreateEvent() , SetEvent()/PulseEvent() , ResetEvent() 等。創建 Event 的時候在 CreateEvent() 的參數中指定 Event 的初始狀態(觸發的 / 未觸發的),還要指定事件是否手動復位(手動復位是隻有用 ResetEvent() 才能把事件狀態顯式地設置爲未觸發的,自動復位是等待該事件的線程等待事件到來之後,系統自動把該事件的狀態復位爲未觸發的)。線程等待事件仍然用 wait函數 。
下面是使用 Event 同步的簡單情況:
圖四、用事件( Event )實現同步
線程 Thread1 執行過程中,要等到某個條件滿足(事件觸發),所以它創建了一個事件 Event (參數設置爲:手動復位,初始條件爲未觸發的),用 WaitForSingleObject() 來等待這個事件。線程 Thread2 執行了一些操作之後,滿足了 Thread1 的條件,用 SetEvent 來觸發該事件。
除了可以用 SetEvent() 來觸發事件之外,也可以用 PulseEvent() 來觸發,區別是 PulseEvent() 觸發該事件之後把它又復位。
另外,也可以把命名事件用於進程之間的同步。實現方法同互斥體中的描述。
六、消息隊列( MsgQueue P2P )
(本節內容適用於 WinCE.net 4.0 及以上版本 )
消息隊列通信機制如同建立了一個管道,管道的雙方通過分別建立到管道的兩端,與管道的讀端口建立連接的進程可以從該端口讀取消息( Message ),與管道的寫端口建立連接的進程可以寫入消息( Message )到管道。管道內消息組成了一個 FIFO ( F irst I n F irst O ut )的隊列,從讀端口讀取消息是讀取隊列的頭,寫入消息到寫端口是在隊列尾部追加一個消息。
WinCE 中關於 MsgQueue 的操作函數主要有:
◇
CreateMsgQueue()
創建一個消息隊列。在該函數的參數中指定消息隊列的名字,消息隊列的最大數目,每個消息的最大長度,對該消息隊列可進行讀還是寫操作等。因爲調用一次
CreateMsgQueue
函數,只能指定讀或者寫這樣的二選一的消息隊列,所以一般需要用相同的消息隊列名字做參數兩次調用該函數,分別創建讀消息隊列和寫消息隊列,它們的返回值分別被讀進程和寫進程用
OpenMsgQueue()
打開用於讀取消息和寫入消息。
◇
OpenMsgQueue()
打開消息隊列並建立與相應端口的連接。進程與讀端口建立連接之後,可用返回的句柄從消息隊列中讀取消息;進程與寫端口建立連接之後,可用返回的句柄寫入消息到消息隊列中。
◇
CloseMsgQueue()
斷開與消息隊列相應的端口之間的連接,並關閉由
CreateMsgQueue()
或
OpenMsgQueue()
創建或打開的消息隊列。
◇
ReadMsgQueue()
如同從普通文件中讀取數據一樣,用於從消息隊列中讀取消息。可以指定讀取消息時,如果消息隊列爲空,讀進程是被掛起還是直接返回。
◇
WriteMsgQueue()
如同寫數據到普通文件中一樣,用於寫消息到消息隊列中。可以指定寫入消息時,如果消息隊列已滿,寫進程是被掛起還是直接返回。
下圖是 MsgQueue 應用的典型場景。
圖五、用消息隊列( MsgQueue )實現同步
這種場景下的執行過程爲:
◇
主進程
MainProcess
創建了名爲“
Reader/Writer MsgQueue
”的讀和寫的消息隊列,並分別返回
hMsgQ_r_m
和
hMsgQ_w_m[
序列
1-4]
。
◇
讀進程
ReaderProcess
以主進程的
ProcessId
和
hMsgQ_r_m
爲參數,通過
OpenMsgQueue()
與
MainProcess
消息隊列的讀端口建立連接
[
序列
5&6]
。
◇
ReaderProcess
與消息隊列建立連接之後,用
WaitForSingleOnject(hMsg_r)
看消息隊列中是否有消息,因爲此時消息隊列爲空,所以
ReaderProcess
被掛起
[
序列
7]
。
◇
寫進程
WriterProcess
以主進程的
ProcessId
和
hMsgQ_w_m
爲參數,通過
OpenMsgQueue()
與
MainProcess
消息隊列的寫端口建立連接
[
序列
8&9]
。
◇
WriterProcess
與消息隊列建立連接之後,用
WaitForSingleOnject(hMsg_w)
看消息隊列中消息是否滿,因爲此時消息隊列爲空,未滿,所以
WriterProcess
不會被掛起
[
序列
10&11]
。
◇
WriterProcess
寫消息到消息隊列中
[
序列
12&13]
。
◇
因爲消息隊列中已經有了消息,
ReaderProcess
從掛起狀態被喚醒
[
序列
14]
。
◇
ReaderProcess
繼續執行,從消息隊列中讀取
WriterProcess
剛纔寫入的消息。
消息隊列除可用於同步之外,主要用於進程之間的數據傳遞,另外消息隊列也可以用於同一進程中的線程之間同步,但是既然線程之間能直接傳遞數據,又何必那麼麻煩呢。
七、互鎖函數( Interlocked Function )
(本節內容適用於 WinCE 1.0 及以上版本)
除了上面各節的同步方法之外, WinCE 還提供了一些用於原子操作的互鎖函數,這些函數在執行過程中,不會因爲線程的調度引起的當前線程被搶佔而打斷函數內的操作。
這些函數主要有:
InterlockedIncrement
InterlockedDecrement
InterlockedExchange
InterlockedTestExchange
InterlockedCompareExchange
InterlockedCompareExchangePointer
InterlockedExchangePointer
InterlockedExchangeAdd
八、 Wait 函數
(本節內容適用於 WinCE 1.0 及以上版本 )
Wait 函數不是特指的某一個函數,而是指 wait 的系列函數。 wait 函數並不是 WinCE 同步機制中的一種,但是 WinCE 的很多同步機制要用到 wait 函數,這些在前面講述各個同步方法的時候也已有論述。
一般地,執行 wait 函數時,如果等待的同步對象條件不滿足,那麼執行 wait 函數的進程 / 線程會被掛起,當然也可以給它們設置等待的超時時間,超過給定時間,不管條件是否滿足,它們會自動從等待狀態甦醒。等待既可以等待某一個條件,也可以等待多個條件中的一個, WinCE 不支持等待多個條件同時滿足,如果有這種需要,要自己實現。
Wait 函數原型如下:
DWORD WaitForSingleObject
(HANDLE hHandle, DWORD dwMilliseconds );
DWORD WaitForMultipleObjects
(
DWORD nCount,
//
No. of object handles in the array.
CONST HANDLE* lpHandles,
//
Pointer to an array of object handles.
BOOL fWaitAll,
// MUST be
FALSE in WinCE
DWORD dwMilliseconds
//
Timeout (0, mills, or INFINITE)
);
DWORD MsgWaitForMultipleObjects
(
DWORD nCount,
//
No. of object handles in the array.
LPHANDLE pHandles,
//
Pointer to an array of object handles.
BOOL fWaitAll,
// MUST be FALSE in WinCE
DWORD dwMilliseconds,
//
Timeout (0, mills, or INFINITE)
DWORD dwWakeMask
//
Input types for which an input event object handle
);
前面講述各種同步機制的時候都是以 WaitForSingleObject() 來說明的,這裏就不再贅述它了。
WaitForMultipleObjects() 和 MsgWaitForMultipleObjects() 可以用來等多個同步對象,它們之間的區別就是 MsgWaitForMultipleObjects() 還等待 dwWakeMask 參數中指定的輸入事件,即這些事件發生時,等待的進程 / 線程也能被喚醒。
用 WaitForMultipleObjects() 等待的多個同步對象的句柄放在參數 lpHandles 數組中,同步對象的句柄的數目放在參數 nCount 中。 dwMilliseconds 指定了等待的超時參數:如果指定爲 0 ,該函數等待每個同步對象之後,不管觸發與否都直接返回;如果指定爲 INFINITE ,該函數等待每個同步對象,直到有一個同步對象被觸發,否則執行該函數的運行實體將一直被掛起;如果指定爲非 0 ,非 INFINITE 的一個數值,那麼不管 等待的同步對象是否被觸發,到了指定的時間,執行該函數而被掛起的運行實體也會被喚醒。因哪個同步對象被觸發而返回還是因超時而返回,可以從返回值中來判定,返回值爲 WAIT_TIMEOUT ,是因爲超時; 返回值爲 WAIT_OBJECT_0 到 WAIT_OBJECT_0 + nCount -1 之間的數時,可以按順序找到具體那個同步對象被觸發 。
下面是 WaitForMultipleObjects 的典型應用。
HANDLE hSynchObjects[EVENT_COUNT];
DWORD dwEvent;
/* Put event handles in hEvents */
// ...
dwEvent = WaitForMultipleObjects (
EVENT_COUNT,
// Number of objects in an array
hSynchObjects,
// Array of objects
FALSE,
// MUST be FALSE
500);
// timeout, 0.5s
switch (dwEvent)
{
case WAIT_TIMEOUT:
// Handle for timeout
break;
case WAIT_OBJECT_0 + 0:
// Handle the 1st event
break;
case WAIT_OBJECT_0 + 1:
// Handle the 2nd one
break;
...
case WAIT_OBJECT_0 + EVENT_COUNT -1:
// Handle the final one
break;
default:
// Error: Not an anticipant one, handle it.
break;
}
總結
本文探討了 WinCE 中的各種同步機制的用法,並給出了它們的典型應用場景。關於它們進一步的高級話題,將在後續文章中探討。
參考資料以及進一步閱讀
1) MSDN
2) UML Reference Manual, 2nd
Edition
3
) Abraham Silberschatz, Peter Baer Galvin, Greg Gagne.
Operating System Concepts, 6th
Edition. John Wiley & Sons, Inc/
高等教育出版社影印
, 2002.5
4
) David R. Butenhof/
於磊,曾剛
. Programming with POSIX Threads. Addison Wesley/
中國電力出版社
, 2003
關於作者
田海立 ,碩士,國家系統分析師,中國系統分析員協會顧問團專業顧問。您可以通過 [email protected] 或 [email protected] 與他聯繫,到 http://blog.csdn.net/thl789/ 看他最新的文章。
版權聲明:
◇
本文爲作者原創作品,版權歸作者所有。
◇
爲了學習和研究,可轉載本文,但必須與原文的內容和格式保持一致,並給出原文的鏈接!
http://blog.csdn.net/thl789/archive/2006/01/17/582246.aspx