程式扎記: [ Windows DDP ] 驅動程式的同步處理 : 核心模式下的同步物件 - 其他同步方法

標籤

2011年2月10日 星期四

[ Windows DDP ] 驅動程式的同步處理 : 核心模式下的同步物件 - 其他同步方法

前言 : 
之前的內容介紹了內核中主要的同步物件. 除了使用同步物件, 還有幾種方法可以實現同步. 這裡將對這幾種方法進行簡介. 

使用自旋鎖進行同步 : 
在驅動程式中, 經常使用自旋鎖作為一種有效的同步機制. 例如在應用程式中打開一個裝置後, 有時候需要該啟多個執行緒去操作裝置 (例如都呼叫 ReadFile 函式對裝置進行讀取操作). 這時後 IRP_MJ_READ 的派遣函式也會同步執行. 但是大部分裝置沒有能力回應同步的讀請求, 必須完成一個讀請求後在完成另一個讀請求. 這時後就須要進行同步處理, 程式設計師可以選擇採用前面介紹的事件, 信號燈, 互斥體等內核物件, 但還有另一種選擇, 這就是使用自旋鎖. 
對於要同步的程式碼, 需要用同一把自旋鎖進行同步. 如果程式得到了自旋鎖, 其他程式希望獲取自旋鎖時, 則不停地進入自旋狀態. 獲得自旋鎖的內核函式是 KeAcquireSpinLock. 直到自旋鎖被釋放後, 另外的程式才能獲取到自旋鎖. 釋放自旋鎖的內核函式是 KeReleaseSpinLock. 如果希望同步某段程式碼區域, 需要在這段程式碼區域前獲取自旋鎖, 在程式碼區域後釋放自旋鎖. 在單 CPU 的系統中, 獲取自旋鎖是透過提升 IRQL 實現的, 而在多 CPU 的系統中, 實現方法比較複雜, 有興緒的讀者可以自行研究. 
無法獲取自旋鎖的執行緒會不停地自旋, 這會浪費很多 CPU 時間. 因此需要同步的程式碼區域不能過長, 換句話說就是佔有自旋鎖的時間不宜太長. 下面程式碼類比了應用程式新建一個裝置後, 同時開啟多個執行緒對裝置進行請求的情況, 這個例子採用的同步機制就是使用自旋鎖 : 

- main.cpp : 使用者模式代碼
  1. #include   
  2. #include   
  3. #include   
  4. #include "Ioctls.h"  
  5. #include   
  6.   
  7. UINT WINAPI SpinLockTest_Fun(LPVOID pContext) {  
  8.     BOOL bRet;  
  9.     DWORD dwOutput;  
  10.     bRet = DeviceIoControl(*(PHANDLE)pContext, IOCTL_SPIN_LOCK, NULL, 0, NULL, 0, &dwOutput, NULL);   
  11.     return 0;  
  12. }  
  13.   
  14. void main()   
  15. {  
  16.     HANDLE hDevice = CreateFile("\\\\.\\HelloDDK",   
  17.                                 GENERIC_READ|GENERIC_WRITE,  
  18.                                 0,  
  19.                                 NULL,  
  20.                                 OPEN_EXISTING,  
  21.                                 FILE_ATTRIBUTE_NORMAL,  
  22.                                 NULL);  
  23.     if(hDevice == INVALID_HANDLE_VALUE)   
  24.     {  
  25.         printf("Failed to obtain file handle to device: "  
  26.             "%s with Win32 error code: %d\n",  
  27.             "MyWDMDevice", GetLastError() );  
  28.         return;  
  29.     }  
  30.     HANDLE hThread[2];  
  31.     // 開啟兩個執行緒, 每個執行緒都去執行 DeviceIoControl  
  32.     // 因此在 IRP_MJ_DEVICE_CONTROL 的派遣函式會並行執行  
  33.     // 為了讓派遣函式不並行處理, 必須進行同步處理  
  34.     // 本例中在派遣函式中採用自旋鎖進行同步  
  35.     hThread[0] = (HANDLE) _beginthreadex( NULL, 0, SpinLockTest_Fun, &hDevice, 0, NULL);  
  36.     hThread[1] = (HANDLE) _beginthreadex( NULL, 0, SpinLockTest_Fun, &hDevice, 0, NULL);  
  37.     WaitForMultipleObjects(2, hThread, TRUE, INFINITE);  
  38.     CloseHandle(hThread[0]);  
  39.     CloseHandle(hThread[1]);  
  40.     CloseHandle(hDevice);  
  41. }  

驅動程式的派遣函式需要進行同步處理, 下面是部份代碼 : 

- Driver.cpp : 對派遣函式使用自旋鎖進行同步
  1. case IOCTL_SPIN_LOCK:  
  2.     {  
  3.         // 為了避免多個派遣函式並行執行, 所以進行同步處理  
  4.         // 此處採用自旋鎖處理同步  
  5.         ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);  
  6.         PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;  
  7.         KIRQL oldirql;  
  8.         KeAcquireSpinLock(gpMy_SpinLock, &oldirql); // 獲取自旋鎖  
  9.         // A 點==============================================================  
  10.         NTSTATUS st = STATUS_SUCCESS;  
  11.         KdPrint(("Enter HelloDDKDeviceIOControl\n"));  
  12.         // 使用自旋鎖後, IRQL 提升到 DISPATCH_LEVEL  
  13.         ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);  
  14.           
  15.         // B 點==============================================================  
  16.         KeReleaseSpinLock(gpMy_SpinLock, oldirql); // 釋放自旋鎖  
  17.         info=0;  
  18.         break;  
  19.     }  

使用互鎖操作進行同步 : 
在本章開始曾經引出一個例子, 來描述同步處理的必要性. 回憶這個例子, C 語言中變數的遞增語句會被編譯器編譯成一段組合語言指令. 例如下面的程式碼在多執行緒環境中, 就存在 "Race condition" 問題. 語句 number++ 不是執行的最小單位, 最小的執行單位是組合語言指令. 每條組合語言指令都有可能被打斷 : 

  1. int number=0;  
  2. void Foo()  
  3. {  
  4.     number++;  
  5.     // do something...  
  6.     number--;  
  7. }  
為了讓 number++ 稱為最小執行單位, 保證執行的原子性, 可以採用很多辦法. 例如可以使用自旋鎖, 下面例子就是更改後的代碼 : 

  1. int number=0;  
  2. void Foo()  
  3. {  
  4.     // 獲取自旋鎖  
  5.     KeAcquireSpinLock(...);  
  6.     number++;  
  7.     // 釋放自旋鎖  
  8.     KeReleaseSpinLock(...);  
  9.   
  10.     // do something...  
  11.   
  12.     // 獲取自旋鎖  
  13.     KeAcquireSpinLock(...);  
  14.     number--;  
  15.     // 釋放自旋鎖  
  16.     KeReleaseSpinLock(...);  
  17. }  
上面的辦法採用自旋鎖進行同步, 需要同步的區域就是 number++ 或是 number--. 對於變數遞增, 遞減需要同步時, 可以使用 DDK 提供的 InterlockedXX 和 ExInterlockedXX 函式. 再將上面程式碼改成下面樣子 : 

  1. int number=0;  
  2. void Foo()  
  3. {  
  4.     // 原子操作  
  5.     InterlockedIncrement(&number);  
  6.   
  7.     // do something  
  8.   
  9.     // 原子操作  
  10.     InterlockedDecrement(&number);  
  11. }  
DDK 提供了兩類互鎖操作來提供簡單的同步處理, 一類是 InterlockedXX 函式, 另一類是 ExInterlockedXX 函式. 其中 InterlockedXX 系列的函示不透過自旋鎖實現, 而 ExInterlockedXX 系列函式透過自旋鎖實現. InterlockedXX 系列函式不需要程式設計師提供自旋鎖, 內部不會提升 IRQL, 因此 InterlockedXX 函式可以操作非分頁的資列, 也可以操作非頁的資料. 而 ExInterlockedXX 需要程式設計師提供一個自旋鎖, 內部依靠這個自旋鎖實現同步, 所有的 ExInterlockedXX 不能操作分頁記憶體的資料.

沒有留言:

張貼留言

網誌存檔

關於我自己

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