程式扎記: [ Windows DDP ] IRP 的同步 : StartIO 常式

標籤

2011年2月21日 星期一

[ Windows DDP ] IRP 的同步 : StartIO 常式


前言 :
StartIO 常式能夠保證各個並行的 IRP 循序執行, 級序列化.

並行執行與序列執行 :
在很多情況下, 對裝置的操作必須是序列執行的, 而不能並存執行. 例如對序列埠的操作, 假如有 N 個執行緒同時操作序列埠裝置時, 必須將這些操作排隊, 然後一一進行處理. 如果不做序列化處理, 當一個操作沒有完畢時, 新的操作又開始, 這必然會導致操作的混亂.
因此驅動程式有必要將並行的請求變成序列的請求, 這需要用到佇列, 如下圖所示. 假如有 8 個讀請求先後來到, 其中每個方塊代表一個請求. 每個方塊的左邊代表發出請求的時間, 寬度代表這個操作所需要的時間 :

可以想像如果不做處理, 派遣函式的執行將會交織再一起, 也就是並行執行. 如果想依次處理每個 IRP, 必須採用佇列將處理序列化. 採用的原則是 "先來先服務". 如下圖所示, 這是排隊以後的請求 :

當一個新的 IRP 請求來臨時, 首先檢查裝置是否處於"忙" 狀態. 裝置在初始化的時候為 "空閒". 當裝置處在 "空閒" 的時候, 可以處理一個 IRP 請求, 並改變當前裝置狀態為 "忙". 如果裝置處於 "忙" 狀態, 則將新來的 IRP 插入佇列, 並立刻返回, IRP 留在以後處理.
當裝置狀態由 "忙" 轉入 "空閒" 狀態時, 則從佇列取出一個 IRP 進行處理, 並重新將狀態變為 "忙". 這樣周而復始地將所有 IRP 的請求序列都處理.

StartIO 常式 :
作業系統為程式設計師提供了一個 IRP 佇列來實現佇列, 這個佇列用 KDEVICE_QUEUE 資料結構表示 :
- Syntax :
  1. typedef struct _KDEVICE_QUEUE {  
  2.     CSHORT Type;  
  3.     CSHORT Size;  
  4.     LIST_ENTRY DeviceListHead;  
  5.     KSPIN_LOCK Lock;  
  6.     BOOLEAN Busy;  
  7. } KDEVICE_QUEUE, *PKDEVICE_QUEUE, *RESTRICTED_POINTER, PRKDEVICE_QUEUE;  

這個佇列的佇列頭保存在裝置物件的 DeviceObject->DeviceQueue 子欄位中. 插入與刪除佇列中的元素都是作業系統負責的. 在使用這個佇列時後, 需要向系統提供一個叫做 StartIo的常式, 並將這個常式的函式名稱傳送給系統, 宣告如下 :
- Syntax :
  1. DRIVER_STARTIO StartIo;  
  2.   
  3. VOID StartIo(  
  4.   __inout  struct _DEVICE_OBJECT *DeviceObject,  
  5.   __in     struct _IRP *Irp  
  6. )  
  7. { ... }  

而你會在 DriverEntry 將這個 StartIo 常式傳遞給 DeviceObject, 程式碼如下 :
  1. extern "C" NTSTATUS DriverEntry (  
  2.                                                      IN PDEVICE_OBJECT pDriverObject,  
  3.                                                      IN PUNICODE_STRING pRegistryPath  
  4.                                                      )  
  5. {  
  6. ...  
  7.     // 設置 StartIO 常式  
  8.     pDeviceObject->DriverStartIo = HelloDDKStartIO;  
  9. ...  
  10. }  
這個 StartIO 常式執行在 DISPATCH_LEVEL 層級, 因此這個函式是不會被執行緒所打斷的. StartIO 常式的參數類似於派遣函式, 只不過沒有返回值. 注意 StartIO 是執行在 DISPATCH_LEVEL 層級上, 因此在宣告時要加上 #pragma LOCKEDCODE 修飾符. 派遣函式如果想把 IRP 序列化, 只要加入 IoStartPacket 函式. 就可以將 IRP 插入佇列了. 並且 IoStartPacket 函式還可以讓程式設計師取消常式. IoStartPacket 首先判斷當前裝置是 "忙" 還是 "空閒". 如果裝置 "空閒", 則提升當前 IRQL 到 DISPATCH_LEVEL 層級, 並進入 StartIO 常式 "序列" 處理該 IRP 請求. 如果裝置 "忙", 則將 IRP 插入佇列後返回. 以下是 IoStartPacket 的偽程式碼 :
  1. VOID IoStartPacket(PDEVICE_OBJECT device, PIRP Irp, PULONG key, PDEVICE_CANCEL cancel)  
  2. {  
  3.     KIRQL oldirql;  
  4.     // 獲得自旋鎖  
  5.     IoAcquireCancelSpinLock(&oldirql);  
  6.     // 設置取消常式  
  7.     IoSetCancelRoutine(Irp, cancel);  
  8.     // 設置 IRP  
  9.     device->CurrentIrp = Irp;  
  10.     // 釋放自旋鎖  
  11.     IoReleaseCancelSpinLock(oldirql);  
  12.     // 呼叫 StartIo 常式  
  13.     device->DriverObject->DriverStartIo(device, Irp);  
  14. }  
在 StartIO 常式結束前, 應該呼叫 IoStartNextPacket 函式, 其作用是從佇列中抽取下一個 IRP, 並將這個 IRP 作為參數呼叫 StartIO 常式. 以下是 IoStartNextPacket 的示例程式碼 :
  1. VOID IoStartNextPacket(PDEVICE_OBJECT device, BOOLEAN cancel)  
  2. {  
  3.     KIRQL oldirql;  
  4.     if(cancel)  
  5.         // 獲取自旋鎖  
  6.         IoAcquireCancelSpinLock(&oldirql);  
  7.     // 刪除裝置佇列    
  8.     PKDEVICE_QUEUE_ENTRY p = KeRemoveDeviceQueue(&device->DeviceQueue);  
  9.     // 獲取 IRP 指標  
  10.     PIRP Irp = CONTAINING_RECORD(p, IRP, Tail.Overlay.DeviceQueueEntry);  
  11.     // 設置 IRP  
  12.     device->CurrentIrp = Irp;  
  13.     if(cancel)  
  14.         // 釋放自旋鎖  
  15.         IoReleaseCancelSpinLock(oldirql);  
  16.     // 呼叫 StartIO 函式  
  17.     device->DriverObject->DriverStartIo(device, Irp);  
  18. }  
從上述程式碼可以看出, 在呼叫 StartIO 之前, 作業系統會將裝置的 device->CurrentIrp 設置為當前 IRP, 這意味這個 IRP 正準備由 StartIO 常式處理. 當處理 StartIO 常式時, 最複雜的莫過於對取消常式的處理, 正確使用 cancel 自旋鎖是關鍵. 在 StartIO 常式的開始處, 應首先獲得 cancel 自旋鎖. 然後判斷當前的 IRP 是否等於 DeviceObject->CurrentIrp. 如果是以上的情況, 說明這個 IRP 正在或者即將被 StartIO 處理. StartIO 常式應立刻釋放自旋鎖, 什麼也不做立刻退出.

示例 :
在使用 StartIO 常式時, 需要 IRP 的派遣函式返回 "Pending" 狀態, 然後呼叫 IoStartPacket 內核函式. 下面的程式碼演示了如何完成 :
- Driver.cpp (函式 HelloDDKRead) :
  1. NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,  
  2.                                  IN PIRP pIrp)   
  3. {  
  4.     KdPrint(("Enter HelloDDKRead\n"));  
  5.   
  6.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)  
  7.             pDevObj->DeviceExtension;  
  8.   
  9.     //將IRP設置為掛起  
  10.     IoMarkIrpPending(pIrp);  
  11.   
  12.     //將IRP插入系統的佇列  
  13.     IoStartPacket(pDevObj,pIrp,0,OnCancelIRP);  
  14.   
  15.     KdPrint(("Leave HelloDDKRead\n"));  
  16.   
  17.     //返回pending狀態  
  18.     return STATUS_PENDING;  
  19. }  

在派遣函式中呼叫 IoStartPacket 內核函式指定取消常式. 下面程式碼演示如何編寫取消常式 :
- Driver.cpp (函式 OnCancelIRP) :
  1. VOID OnCancelIRP(  
  2.     IN PDEVICE_OBJECT DeviceObject,  
  3.     IN PIRP Irp  
  4.     )  
  5. {  
  6.     KdPrint(("Enter CancelReadIRP\n"));  
  7.   
  8.     if (Irp==DeviceObject->CurrentIrp)  
  9.     {  
  10.         //表明當前正在改由StartIo處理  
  11.         //但StartIo並沒有獲取cancel自旋鎖之前  
  12.         //這時候需要  
  13.         KIRQL oldirql = Irp->CancelIrql;  
  14.   
  15.         //釋放Cancel自旋鎖  
  16.         IoReleaseCancelSpinLock(Irp->CancelIrql);  
  17.   
  18.         IoStartNextPacket(DeviceObject,TRUE);  
  19.   
  20.         KeLowerIrql(oldirql);  
  21.     }else  
  22.     {  
  23.         //從裝置佇列中將該IRP抽取出來  
  24.         KeRemoveEntryDeviceQueue(&DeviceObject->DeviceQueue,&Irp->Tail.Overlay.DeviceQueueEntry);  
  25.         //釋放Cancel自旋鎖  
  26.         IoReleaseCancelSpinLock(Irp->CancelIrql);  
  27.     }  
  28.   
  29.       
  30.     //設置完成狀態為STATUS_CANCELLED  
  31.     Irp->IoStatus.Status = STATUS_CANCELLED;  
  32.     Irp->IoStatus.Information = 0;   // bytes xfered  
  33.     IoCompleteRequest( Irp, IO_NO_INCREMENT );  
  34.   
  35.     KdPrint(("Leave CancelReadIRP\n"));  
  36. }  

接著編寫 StartIO 常式, 注意 StartIO 執行在 DISPATCH_LEVEL 層級, 因此不能使用分頁記憶體, 否則會引起頁故障, 從而導致系統崩潰. 下面為演示程式碼 :
- Driver.cpp (函式 HelloDDKStartIO) :
  1. #pragma LOCKEDCODE  
  2. VOID HelloDDKStartIO(  
  3.     IN PDEVICE_OBJECT  DeviceObject,  
  4.     IN PIRP  Irp   
  5.     )  
  6. {  
  7.     KIRQL oldirql;  
  8.     KdPrint(("Enter HelloDDKStartIO\n"));  
  9.   
  10.     //獲取cancel自旋鎖  
  11.     IoAcquireCancelSpinLock(&oldirql);  
  12.     if (Irp!=DeviceObject->CurrentIrp||Irp->Cancel)  
  13.     {  
  14.         //如果當前有正在處理的IRP,則簡單的入佇列,並直接返回  
  15.         //入佇列的工作由系統完成,在StartIO中不用負責  
  16.         IoReleaseCancelSpinLock(oldirql);  
  17.         KdPrint(("Leave HelloDDKStartIO\n"));  
  18.         return;  
  19.     }else  
  20.     {  
  21.         //由於正在處理該IRP,所以不允許呼叫取消常式  
  22.         //因此將此IRP的取消常式設置為NULL  
  23.         IoSetCancelRoutine(Irp,NULL);  
  24.         IoReleaseCancelSpinLock(oldirql);  
  25.     }  
  26.   
  27.     KEVENT event;  
  28.     KeInitializeEvent(&event,NotificationEvent,FALSE);  
  29.   
  30.     //等3秒  
  31.     LARGE_INTEGER timeout;  
  32.     timeout.QuadPart = -3*1000*1000*10;  
  33.   
  34.     //定義一個3秒的延時,主要是為了模擬該IRP操作需要大概3秒左右時間  
  35.     KeWaitForSingleObject(&event,Executive,KernelMode,FALSE,&timeout);  
  36.   
  37.     Irp->IoStatus.Status = STATUS_SUCCESS;  
  38.     Irp->IoStatus.Information = 0;   // no bytes xfered  
  39.     IoCompleteRequest(Irp,IO_NO_INCREMENT);  
  40.   
  41.   
  42.     //在佇列中讀取一個IRP,並進行StartIo  
  43.     IoStartNextPacket(DeviceObject,TRUE);  
  44.   
  45.     KdPrint(("Leave HelloDDKStartIO\n"));  
  46. }  

最後編寫應用程式的程式碼. 這段程式碼首先打開裝置, 然後新建兩個執行緒, 每個執行緒都是非同步讀取. 為了模擬真實的情況, 這裡在 StartIO 中停頓了 3 秒鐘. 應用程式同步的發起兩個執行緒同時讀取裝置, 從而真實模擬了同步發起 IRP 請求 :
- main.cpp :
  1. #include   
  2. #include   
  3. #include   
  4.   
  5. UINT WINAPI Thread(LPVOID context)  
  6. {  
  7.     printf("Enter Thread\n");  
  8.     //等待5秒  
  9.     OVERLAPPED overlap={0};  
  10.     overlap.hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);  
  11.     UCHAR buffer[10];  
  12.     ULONG ulRead;  
  13.       
  14.     BOOL bRead = ReadFile(*(PHANDLE)context,buffer,10,&ulRead,&overlap);  
  15.   
  16.     //可以試驗取消常式  
  17.     //CancelIo(*(PHANDLE)context);  
  18.     WaitForSingleObject(overlap.hEvent,INFINITE);  
  19.     return 0;  
  20. }  
  21.   
  22. int main()  
  23. {  
  24.     HANDLE hDevice =   
  25.         CreateFile("\\\\.\\HelloDDK",  
  26.                     GENERIC_READ | GENERIC_WRITE,  
  27.                     FILE_SHARE_READ,  
  28.                     NULL,  
  29.                     OPEN_EXISTING,  
  30.                     FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此處設置FILE_FLAG_OVERLAPPED  
  31.                     NULL );  
  32.   
  33.     if (hDevice == INVALID_HANDLE_VALUE)  
  34.     {  
  35.         printf("Open Device failed!");  
  36.         return 1;  
  37.     }  
  38.   
  39.     HANDLE hThread[2];  
  40.     hThread[0] = (HANDLE) _beginthreadex (NULL,0,Thread,&hDevice,0,NULL);  
  41.     hThread[1] = (HANDLE) _beginthreadex (NULL,0,Thread,&hDevice,0,NULL);  
  42.   
  43.     //主執行緒等待兩個子執行緒結束  
  44.     WaitForMultipleObjects(2,hThread,TRUE,INFINITE);  
  45.       
  46.     //新建IRP_MJ_CLEANUP IRP  
  47.     CloseHandle(hDevice);  
  48.   
  49.     return 0;  
  50. }  

執行結果如下圖所示 :
This message was edited 7 times. Last update was at 17/01/2011 14:12:31

沒有留言:

張貼留言

網誌存檔

關於我自己

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