程式扎記: [ Windows DDP ] 驅動程式的同步處理 : 使用者模式下的同步物件

標籤

2011年1月20日 星期四

[ Windows DDP ] 驅動程式的同步處理 : 使用者模式下的同步物件


前言 :
在核心模式下可以使用很多種內核同步物件, 這些內核同步物件和使用者模式下的同步物件非常類似. 同步物件包含事件 (Event), 互斥體 (Mutex), 信號燈 (Semaphore) 等. 為了和核心模式同步物件相對比, 這裡會先介紹使用者模式下同步物件的使用方法.
最後在學習完內核同步物件後, 讀者會發現使用者模式的同步物件都是借用核心模式的同步物件實現的. 使用者模式下的同步物件其實是核心模式下同步物件的再次封裝.

使用者模式的等待 :
在應用程式中, 可以使用 WaitForSingleObject 和 WaitForMultipleObjects 等待同步物件. 其中 WaitForSingleObject 用於等待同一個同步物件, 而 WaitForMultipleObjects 用於等待多個同步物件. WaitForSingleObject 函式宣告如下 :
- Syntax :
  1. DWORD WINAPI WaitForSingleObject(  
  2.   __in  HANDLE hHandle,  
  3.   __in  DWORD dwMilliseconds  
  4. );  

第二個參數 dwMilliseconds 是等待時間, 單位是 ms. 同步物件有兩種狀態, 一種是激發狀態, 一種是未激發狀態. 如果同步物件處於未激發狀態, WaitForSingleObject 則進入休眠, 等待同步物件被激發. 如果同步物件在指定的等待時間內, 還沒有處於激發狀態, 則自動停止休眠. dwMilliseconds 也可以設定為 INFINITE, 這表示無限期地等待下去. 另外 dwMilliseconds 也可以為 0, 其作用是強迫作業系統將當前執行緒切換到其他執行緒.
WaitForMultipleObjects 函式宣告如下 :
- Syntax :
  1. DWORD WINAPI WaitForMultipleObjects(  
  2.   __in  DWORD nCount,  // 同步物件陣列元素個數  
  3.   __in  const HANDLE *lpHandles,  // 同步物件陣列指標  
  4.   __in  BOOL bWaitAll,  // 是否等待全部同步物件  
  5.   __in  DWORD dwMilliseconds  // 等待時間 (ms)  
  6. );  

和 WaitForSingleObject 函式略有不同, WaitForMultipleObjects 可以等待多個同步物件, 它需要提供一個同步物件的陣列, 如果參數 bWaitAll 為真, 則等待所有的同步物件. 否則只要一個同步物件被激發, 則執行緒恢復執行.

使用者模式開啟多執行緒 :
等待同步物件一般出現在多執行緒的程式設計中, 因此這裡介紹一下應用程式如何新建新執行緒. Win32 API CreateThread 函式負責新建新執行緒 :
- Syntax :
  1. HANDLE WINAPI CreateThread(  
  2.   __in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes,  // 安全屬性  
  3.   __in       SIZE_T dwStackSize,                        // 初始化堆疊大小  
  4.   __in       LPTHREAD_START_ROUTINE lpStartAddress,     // 執行緒執行的函式指標  
  5.   __in_opt   LPVOID lpParameter,                        // 傳入函式中的參數  
  6.   __in       DWORD dwCreationFlags,                 // 開啟執行緒時的狀態  
  7.   __out_opt  LPDWORD lpThreadId                     // 返回執行緒 ID  
  8. );  

CreateThread 用 lpStartAddress 參數指定新執行緒的執行位址, 用 dwStackSize 參數指定新執行緒的堆疊大小, 用 lpParameter 參數指定新執行緒的參數資訊.
一些舊的執行時期函式不支援多執行緒. 執行時期函式中使用了大量的全域變數和靜態變數, 如全域變數和靜態變數沒有用同步機制保護, 就會導致函式在多執行緒環境下執行錯誤. 這種錯誤可以說成函式是 "不可重入" 的.
幸運的是, 微軟編寫了一套多執行緒版本的執行時期函式程式庫. 在使用這個版本的執行時期程式庫時, 需要指定連結這些程式庫, 些改方法如下圖 :

另外新建多執行緒的時候最好不要使用 CreateThread 函式, 而使用 _beginthreadex 函式. _beginthreadex 函式是對 CreateThread 函式進行封裝, 其參數與 CreateThread 完全一致. _beginthreadex 函式的函式名前有個底線, 是因為它不是標準 C 語言提供的執行時期函式.
另外許多協力廠商程式庫都會對 CreateThread 函式進行封裝, 從而達到方便程式設計師使用目的. 例如 MFC 程式庫提供了 CWinThread 類別, 這個類別對執行緒的各個操作函式進行了封裝.

使用者模式的事件 :
事件是一種典型的同步物件. 使用者模式下的事件和核心模式的事件緊密相連. 在使用事件之前, 需要對事件進行初始化, 使用 CreateEvent API 函式 :
- Syntax :
  1. HANDLE WINAPI CreateEvent(  
  2.   __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,    // 安全屬性  
  3.   __in      BOOL bManualReset,                      // 是否設為手動  
  4.   __in      BOOL bInitialState,                     // 初始狀態  
  5.   __in_opt  LPCTSTR lpName                      // 命名  
  6. );  

所有形如 CreateXXX 的 Win32 API 函式, 如果它的第一個參數是 LPSECURITY_ATTRIBUTES 類型, 那麼這種 API 內部都會新建一個對應的內核物件. 這種 API 返回一個控制碼, 作業系統可以透過這個控制碼找到具體的內核物件. 而 CreateEvent 函式就是這樣的函式, 它內部會使用作業系統新建一個內核事件物件. CreateEvent 返回的控制碼值就代表了這個內核事件物件. 應用程式無法獲得這個內核事件物件的指標, 而用一個控制碼 (32 位元不帶符號的整數) 代表事件物件.
一般情況下, CreateEvent 的安全屬性設置為 NULL. 它的第二個參數 bManualReset 表示新建的事件是否為手動模式. 如果是手動模式的事件, 事件處於激發狀態後, 需要手動設置才能回到未激發狀態. 如果是自動模式, 當事件處於激發狀態後, 遇到任義一個等待 (如 WaitForSingleObject), 則自動變回未激發狀態. 下面的例子展示了前面介紹過的 API 函式, 包括新建執行緒, 新建事件與等待事件等.
在這個例子的主執行緒中, 開啟新的輔助執行緒, 主執行緒把一個事件的控制碼傳遞給子執行緒. 同時主執行緒等待該事件激發, 輔助執行緒所做的事情就是顯示一些資訊, 並設置該事件. 這個例子非常簡單, 但也說明了同步處理的重要性. 如果主執行緒不等待事件, 也是以非同步的方式共同和輔助執行緒執行, 這時很有可能主執行緒都退出來, 而輔助執行緒還繼續在執行.
- 範例代碼 : 示範 CreateEvent, _beginthreadex, WaitForSingleObject 等函式使用方法.
  1. #include   
  2. #include   /*_beginthread, _endthread*/  
  3. #include   
  4. #include   
  5. #include   
  6. #include   
  7.   
  8. UINT WINAPI Fun1(LPVOID para) {  
  9.     printf("Enter Fun1\n");  
  10.     HANDLE* phEvent = (HANDLE*)para;  
  11.     // 設置該事件觸發  
  12.     SetEvent(*phEvent);  
  13.     printf("Leave Fun1\n");  
  14.     return 0;     
  15. }  
  16.   
  17.   
  18. int main() {  
  19.     // 新建同步事件  
  20.     HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);  
  21.     // 開啟新執行緒, 並將同步事件控制碼指標傳遞給新執行緒  
  22.     HANDLE hThread = (HANDLE) _beginthreadex(NULL, 0, Fun1, &hEvent, 0, NULL);  
  23.     // 等待該事件觸發  
  24.     WaitForSingleObject(hEvent, INFINITE);  
  25.     system("pause");  
  26.     return 0;  
  27. }  

使用者模式的信號燈 :
信號燈也是一種常用的同步物件, 信號燈也有兩種狀態 (激發與未激發). 信號燈內部有個計數器, 可以理解信號燈內部有 N 個燈泡. 如果有一個燈泡亮著, 就代表信號燈處於激發狀態, 如果全滅則代表信號燈處於未激發狀態. 先新建信號燈可以透過 CreateSemaphore 函式, 它的宣告如下 :
- Syntax :
  1. HANDLE WINAPI CreateSemaphore(  
  2.   __in_opt  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  
  3.   __in      LONG lInitialCount,  
  4.   __in      LONG lMaximumCount,  
  5.   __in_opt  LPCTSTR lpName  
  6. );  

其中第二個參數 lInitialCount 在指明初始化的時候, 計數器的值為多少. 第三個參數 lMaximumCount 指明該信號燈計數器最大的值為多少. 如果初始值為 0, 則處於未激發狀態. 如果初始值為非零, 則處於激發狀態. 另外可以使用 ReleaseSemaphore 函式增加信號燈的計數器, 其函式宣告如下 :
- Syntax :
  1. BOOL WINAPI ReleaseSemaphore(  
  2.   __in       HANDLE hSemaphore,  
  3.   __in       LONG lReleaseCount,  
  4.   __out_opt  LPLONG lpPreviousCount  
  5. );  

其中第二個參數 lReleaseCount 是這次操作增加計數的數量, 第三個參數 lpPreviousCount 獲得執行本操作前計數的大小. 另外對信號燈執行一次等待操作, 就會減少一個計數, 相當熄滅一個燈泡. 當計數為零時, 也就是所有的燈泡都熄滅時, 當前執行緒進入睡眠狀態, 直到信號燈變為激發狀態. 下面綜合以上的 API 編寫了信號燈同步物件使用範例 :
- 範例程式 : 信號燈使用範例
  1. #include   
  2. #include   /* _beginthread, _endthread*/  
  3. #include   
  4. #include   
  5.   
  6.   
  7. UINT WINAPI Fun1(LPVOID para) {  
  8.     long dValue=0;  
  9.     printf("\tEnter Fun1\n");  
  10.     HANDLE* phSemapore = (HANDLE*)para;  
  11.     // 等待 5 秒  
  12.     Sleep(5000);  
  13.     printf("\tLeave Fun1\n");  
  14.     ReleaseSemaphore(*phSemapore, 1, &dValue);  
  15.     //printf("\tDefault value=%d\n", dValue);  
  16.     return 0;  
  17. }  
  18.   
  19. int main() {  
  20.     printf("Enter main\n");  
  21.     HANDLE hSemaphore = CreateSemaphore(NULL, 11, NULL);  
  22.     // 此時的信號燈計數為1, 處於觸發狀態  
  23.     WaitForSingleObject(hSemaphore, INFINITE);  
  24.     // 此時的信號燈計數為0, 處於未觸發狀態  
  25.     // 開啟新執行緒, 並將同步事件控制碼傳遞給新執行緒  
  26.     HANDLE hThread1 = (HANDLE) _beginthreadex(NULL, 0, Fun1, &hSemaphore, 0, NULL);  
  27.     // 等待該事件觸發.  
  28.     //printf("Wait...\n");  
  29.     WaitForSingleObject(hSemaphore, INFINITE);  
  30.     printf("Leave main\n");  
  31.     system("pause");  
  32.     return 0;  
  33. }  

使用者模式的互斥體 :
互斥體也是一種常用的同步物件. 互斥體可以避免多個執行緒爭奪同一個資源. 例如多執行環境中, 只能有一個執行緒佔有互斥體. 獲得互斥體的執行緒如果不釋放互斥體, 其他執行緒永遠不會獲得這個互斥體. 互斥體的概念類似於同步事件, 所不同的是同一個執行緒可以遞迴獲得互斥體. 遞迴獲得互斥體的意思是得到互斥體的執行緒還可以再次獲得這個互斥體, 或者說互斥體對於已經獲得互斥體的執行緒不會產生 "互斥" 關係, 而同步事件不能遞迴獲取. 初始化互斥體的函式是 CreateMutex, 其函式宣告如下 :
- Syntax :
  1. HANDLE WINAPI CreateMutex(  
  2.   __in_opt  LPSECURITY_ATTRIBUTES lpMutexAttributes,  
  3.   __in      BOOL bInitialOwner,  
  4.   __in_opt  LPCTSTR lpName  
  5. );  

其中第二個參數 bInitialOwner 表明初始化時是否被佔有. 如果沒有被佔有, 則為激發狀態, 否則為未激發狀態. 另外獲得互斥體的函式是 WaitForMultipleObjects 函式, 而釋放互斥體的函式是 ReleaseMutex 函式.
下面的例子展示如何使用互斥體. 例中主執行緒新建兩個執行緒, 且兩個執行緒先各自執行一段時間. 因為內核的調度, 實際上兩個執行緒的執行會交織起來, 為了讓兩個執行緒不出現交織執行, 這裡使用互斥體作為同步處理機制 :
- 範例代碼 : 如何使用互斥體
  1. #include   
  2. #include  /* _ beginthread, _endthread*/  
  3. #include   
  4.   
  5. UINT WINAPI Fun1(LPVOID para) {  
  6.     HANDLE* pMutex = (HANDLE*) para;  
  7.     WaitForSingleObject(*pMutex, INFINITE);  
  8.     printf("\tEnter Fun1...\n");  
  9.     Sleep(2000);  
  10.     printf("\tLeave Fun1...\n");  
  11.     ReleaseMutex(*pMutex);  
  12.     return 0;  
  13. }  
  14.   
  15. UINT WINAPI Fun2(LPVOID para) {  
  16.     HANDLE* pMutex = (HANDLE*) para;  
  17.     WaitForSingleObject(*pMutex, INFINITE);  
  18.     printf("\tEnter Fun2...\n");  
  19.     Sleep(2000);  
  20.     printf("\tLeave Fun2...\n");  
  21.     ReleaseMutex(*pMutex);  
  22.     return 0;  
  23. }  
  24.   
  25. int main() {  
  26.     printf("Enter main...\n");  
  27.     // 新建同步事件  
  28.     HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);  
  29.     // 開啟新執行緒, 並將同步事件控制碼傳遞給新執行緒.  
  30.     HANDLE hThread1 = (HANDLE) _beginthreadex(NULL, 0, Fun1, &hMutex, 0, NULL);  
  31.     HANDLE hThread2 = (HANDLE) _beginthreadex(NULL, 0, Fun2, &hMutex, 0, NULL);  
  32.     // 強迫等 6 秒, 讓執行緒1 和 執行緒2 執行完畢  
  33.     Sleep(6000);  
  34.     printf("Leave main...\n");  
  35.     system("pause");  
  36.     return 0;  
  37. }  

等待中的執行緒 :
還有一種同步物件, 其實前面已經看過, 這就是執行緒物件. 每個執行緒同樣有兩個狀態, 當執行緒處於執行的時候, 是未激發狀態 ; 當執行緒終止後, 執行續處於激發狀態. 可以用 WaitXXX 函式對執行緒控制碼進行等待. 下面例子展示如何等待所有執行緒的結束, 範例中主執行緒建立兩個新的執行緒, 主執行緒用 WaitForMultipleObjects 等待兩個子執行緒全部執行結束 :
範例代碼 : 等待所有執行緒執行完畢
  1. #include   
  2. #include   
  3. #include   
  4. #include   
  5.   
  6. UINT WINAPI Fun1(LPVOID para) {  
  7.     printf("Enter Fun1\n");  
  8.     Sleep(1000);  
  9.     printf("Leave Fun1\n");  
  10.     return 0;  
  11. }  
  12.   
  13. int main() {  
  14.     HANDLE hThread[2];  
  15.     hThread[0] = (HANDLE) _beginthreadex(NULL, 0, Fun1, NULL, 0, NULL);  
  16.     hThread[1] = (HANDLE) _beginthreadex(NULL, 0, Fun1, NULL, 0, NULL);  
  17.     WaitForMultipleObjects(2, hThread, TRUE, INFINITE);  
  18.     system("pause");  
  19.     return 0;  
  20. }  
This message was edited 10 times. Last update was at 06/01/2011 18:11:15

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!