程式扎記: [ Windows DDP ] IRP 的同步 : IRP 同步/非同步完成

標籤

2011年2月18日 星期五

[ Windows DDP ] IRP 的同步 : IRP 同步/非同步完成


前言 :
前一節介紹了如何在應用程式中對裝置進行同步, 非同步作業. 但是這些同步, 非同步作業必須得到驅動程式的支援. 所有裝置的操作都會轉化為 IRP 請求, 並傳遞到對應的派遣函式中. 可以有兩種方式處理 IRP 請求, 第一種是在派遣函式中直接結束 IRP 請求, 可以認為這是一種同步處理的方法, 前面介紹的例子都是基於這種處理方式, 也是最簡單的處理方式. 另一種方法是在派遣函式中不結束 IRP 請求, 而是讓派遣函式直接返回. IRP 在以後的某個時候在進行處理. 非同步處理是這裡要介紹的內容.

IRP 的同步完成 :
下面將介紹 Win32 API 函式是如何一層層透過呼叫進入到派遣函式 :
1. 在應用程式中呼叫 CreateFile Win32 API 函式, 這個函式打開裝置.
2. CreateFile Win32 API 函式內部呼叫了 ntdll.dll 中的 NtCreateFile 函式.
3. ntdll.dll 中的 NtCreateFile 函式進入核心模式, 然後呼叫 ntoskrnl.exe 中的 NtCreateFile 函式.
4. 核心模式中 ntoskrnl.exe 中的 NtCreateFile 函式新建 IRP_MJ_CREATE 類型的 IRP, 然後呼叫對應驅動程式的派遣函式, 並將 IRP 的指標傳遞給該派遣函式.
5. 派遣函式呼叫 IoCompleteRequest 將 IRP 請求結束.
6. 作業系統按原路返回, 一直到退出 CreateFile Win32 API 函式. 至此 CreateFile 函式返回.
7. 如果需要讀取裝置, 應用程式會呼叫 ReadFile Win32 API 函式.
8. ReadFile Win32 API 會呼叫 ntdll.dll 中的 NtReadFile 函式.
9. ntdll.dll 中的 NtReadFile 函式會進入核心模式, 並呼叫 ntoskrnl.exe 中的 NtReadFile 函式.
10. ntoskrnl.exe 中的 NtReadFile 函式新建 IRP_MJ_READ 類型的 IRP 並將之傳入對應的派遣函式中.

對裝置進行讀取可以有三種方式, 第一種是用 ReadFile 函式進行同步讀取, 第二種方式是用 ReadFile 函式進行非同步讀取. 第三種方法是用 ReadFileEx 函式進行非同步讀取.
- 用 ReadFile 函式進行同步讀取
1. ReadFile 函式內部會新建一個事件, 這個事件連同 IRP 一起被傳遞到派遣函式中 (這個事件是 IRP 的 UserEvent 子欄位)
2. 派遣函式呼叫 IoCompleteRequest 時, IoCompleteRequest 內部會設置 IRP 的 UserEvent 事件.
3. 作業系統按照原路一直返回到 ReadFile 函式, ReadFile 函式會等待這個事件, 因為該事件已經被設置, 所以無須等待.
4. 如果在派遣函式沒有呼叫 IoCompleteRequest 函式, 該事件就沒有被設置, ReadFile 會一直等待 IRP 被結束.

- 用 ReadFile 函式進行非同步讀取
1. 這時 ReadFile 內部不會新建事件, 但 ReadFile 函式會接收 overlap 參數. overlap 參數中會提供一個事件, 這個事件會被用做同步處理.
2. IoCompleteRequest 內部會設置 overlap 提供的事件.
3. 在 ReadFile 函式退出前, 它不會檢測該事件是否被設置, 因此可以不等待操作是否真的完成.
4. 當 IRP 操作完成後, overlap 提供的事件被設置, 這個事件會通知應用程式 IRP 請求被完成.

- 用 ReadFileEx 函式進行非同步讀取
1. ReadFileEx 不提供事件, 但提供一個回呼函式, 這個回呼函式的位址會被作為 IRP 參數傳遞給派遣函式.
2. IoCompleteRequest 會將這個完成函式插入 APC 佇列.
3. 應用程式進入 Alert 狀態, APC 佇列會自動出佇列, 完成函式會被執行, 這相當於通知應用程式操作已經完成.

IRP 的同步處理就是在派遣函式中, 將 IRP 處理完畢. 這裡指處理完畢就是呼叫 IoCompleteRequest 函式.

IRP 的非同步完成 :
IRP 被 "非同步完成" 指的就是不在派遣函式中呼叫 IoCompleteRequest 內核函式. 呼叫 IoCompleteRequest 函式意味 IRP 請求的結束, 也標誌本次對裝置操作的結束.
IRP 是被非同步完成, 而發起 IRP 的應用程式會有三種型式發起 IRP 請求, 分別是用 ReadFile 函式同步讀取裝置, 用 ReadFile 函式非同步讀取裝置, 用 ReadFileEx 非同步讀取裝置. 以下分別列出這三種方式 :
* IRP 是由 ReadFile 的同步操作引起 :
當派遣函式退出時, 因為 IoCompleteRequest 被呼叫而結束此次 IRP 請求. 也就是說 ReadFile 會等待派遣函式執行完畢.

* IRP 是由 ReadFile 的非同步作業引起 :
當派遣函式退出時, 由於 IoCompleteRequest 沒有被呼叫, IRP 請求沒有被結束. 但是 ReadFile 會立刻返回且返回值為失敗, 代表操作沒有完成. 透過呼叫 GetLastError 函式, 可以得到這時的錯誤程式碼是 ERROR_IO_PENDING. 這不是真正的操作錯誤, 而意味 ReadFile 並沒有真正完成操作, ReadFile 只是非同步的返回. 當 IRP 請求被結束, 即呼叫了 IoCompleteRequest, ReadFile 函式提供的 overlap 的事件才會被設置. 這個事件可以通知應用程式 ReadFile 的請求真正被執行完畢.

* IRP 是由 ReadFileEx 的非同步作業引起 :
和 ReadFile 的非同步類似, ReadFileEx 會立即返回, 但返回值是 FALSE, 說明讀取沒有成功. 這時後如果呼叫 GetLastError 函式, 會發現錯誤是 ERROR_IO_PENDING, 表明當前操作未完成. 當 IRP 被結束後, 即呼叫了 IoCompleteRequest 後, ReadFileEx 提供的回呼函式會被插入到 APC 佇列中. 一旦作業系統進入 Alert 狀態, 執行緒的 APC 佇列會自動出佇列, 進而 ReadFileEx 提供的回呼函式被呼叫, 這相當於通知應用程式操作完畢.

如果派遣函式不呼叫 IoCompleteRequest 函式, 則需要告訴作業系統此 IRP 處於 "Pending" 狀態. 這時需要呼叫內核函式 IoMarkIrpPending. 同時派遣函式應該返回 STATUS_PENDING. 下面為範例代碼 :
  1. NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,  
  2.                                         IN PIRP pIrp)  
  3. {  
  4.     //....  
  5.     IoMarkIrpRequest(pIrp);  
  6.     // 返回 Pending 狀態  
  7.     return STATUS_PENDING;  
  8. }  
為了演示非同步處理 IRP, 假設 IRP_MJ_READ 的派遣函式僅僅是返回 "Pending" . 應用程式關閉裝置的時後會產生 IRP_MJ_CLEANUP 類型的 IRP. 在 IRP_MJ_CLEANUP 的派遣函式中結束那些 "Pending" 的 IRP_MJ_READ.
為了能儲存有哪些 IRP_MJ_READ IRP 被 "Pending", 這裡使用一個佇列, 也就把每個掛起的 IRP_MJ_READ 的指標都插入佇列, 最後 IRP_MJ_CLEANUP 的派遣函式將一個個 IRP 取出佇列並且呼叫 IoCompleteRequest 函式將它們結束. 首先要定義好佇列的資料結構, 該資料結構僅有一個子欄位來記錄 IRP 指標 :
  1. typedef struct _MY_IRP_ENTRY  
  2. {  
  3.     PIRP pIRP; // 記錄 IRP 指標  
  4.     LIST_ENTRY ListEntry;  
  5. } MY_IRP_ENTRY, *PMY_IRP_ENTRY;  
在裝置擴充中加入 "佇列" 這個變數, 這樣驅動程式的所有派遣函式都可以使用該佇列. 在 DriverEntry 中初始化該佇列, 並在 DriverUnload 常式中回收佇列. 在 IRP_MJ_READ 的派遣函式中, 將 IRP 插入堆疊, 然後返回 "Pending" 狀態. 下面程式碼示範了非同步處理 IRP 的派遣函式 :
  1. NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,  
  2.                                          IN PIRP pIrp)  
  3. {  
  4.     KdPrint(("Enter HelloDDKRead\n"));  
  5.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;  
  6.     PMY_IRP_ENTRY pIrp_entry = (PMY_IRP_ENTRY) ExAllocatePool(PagedPool, sizeof(MY_IRP_ENTRY));  
  7.     pIrp_entry->pIrp = pIrp;  
  8.     // 插入佇列  
  9.     InsertHeadList(pDevExt->pIRPLinkListHead, &pIrp_entry->ListEntry);  
  10.     // 將 IRP 設置為 "Pending"  
  11.     IoMarkIrpPending(pIrp);  
  12.     KdPrint(("Leave HelloDDKRead\n"));  
  13.     // 返回 Pending 狀態  
  14.     return STATUS_PENDING;  
  15. }  
再關閉裝置的時候, 會產生 IRP_MJ_CLEANUP 類型的 IRP. 其派遣函式抽取佇列中每一個 "Pending" 的 IRP, 並呼叫 IoCompleteRequest 設置完成.
- Driver.cpp (函式 HelloDDKCleanUp) : 清除 Pending 的 IRP
  1. NTSTATUS HelloDDKCleanUp(IN PDEVICE_OBJECT pDevObj,  
  2.                                  IN PIRP pIrp)   
  3. {  
  4.     KdPrint(("Enter HelloDDKCleanUp\n"));  
  5.   
  6.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)  
  7.             pDevObj->DeviceExtension;  
  8.       
  9.     //(1)將存在佇列中的IRP逐個出佇列,並處理  
  10.   
  11.     PMY_IRP_ENTRY my_irp_entry;  
  12.     while(!IsListEmpty(pDevExt->pIRPLinkListHead))  
  13.     {  
  14.         PLIST_ENTRY pEntry = RemoveHeadList(pDevExt->pIRPLinkListHead);  
  15.         my_irp_entry = CONTAINING_RECORD(pEntry,  
  16.                               MY_IRP_ENTRY,  
  17.                               ListEntry);  
  18.         my_irp_entry->pIRP->IoStatus.Status = STATUS_SUCCESS;  
  19.         my_irp_entry->pIRP->IoStatus.Information = 0// bytes xfered  
  20.         IoCompleteRequest( my_irp_entry->pIRP, IO_NO_INCREMENT );  
  21.   
  22.         ExFreePool(my_irp_entry);  
  23.     }  
  24.       
  25.     //(2)處理IRP_MJ_CLEANUP的IRP  
  26.     NTSTATUS status = STATUS_SUCCESS;  
  27.     // 完成IRP  
  28.     pIrp->IoStatus.Status = status;  
  29.     pIrp->IoStatus.Information = 0;  // bytes xfered  
  30.     IoCompleteRequest( pIrp, IO_NO_INCREMENT );  
  31.   
  32.     KdPrint(("Leave HelloDDKCleanUp\n"));  
  33.     return STATUS_SUCCESS;  
  34. }  

在應用程式中非同步作業該裝置, 先非同步讀兩次, 這樣會新建兩個 IRP_MJ_READ. 接著這兩個 IRP 被插入佇列. 在關閉裝置時, 會導致驅動程式呼叫 IRP_MJ_CLEAN_UP 的派遣函式 :
- main.cpp :
  1. #include   
  2. #include   
  3.   
  4. int main()  
  5. {  
  6.     HANDLE hDevice =   
  7.         CreateFile("\\\\.\\HelloDDK",  
  8.                     GENERIC_READ | GENERIC_WRITE,  
  9.                     0,  
  10.                     NULL,  
  11.                     OPEN_EXISTING,  
  12.                     FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//森揭扢离FILE_FLAG_OVERLAPPED  
  13.                     NULL );  
  14.   
  15.     if (hDevice == INVALID_HANDLE_VALUE)  
  16.     {  
  17.         printf("Open Device failed!");  
  18.         return 1;  
  19.     }  
  20.   
  21.     OVERLAPPED overlap1={0};  
  22.     OVERLAPPED overlap2={0};  
  23.   
  24.     UCHAR buffer[10];  
  25.     ULONG ulRead;  
  26.       
  27.     BOOL bRead = ReadFile(hDevice,buffer,10,&ulRead,&overlap1);  
  28.     if (!bRead && GetLastError()==ERROR_IO_PENDING)  
  29.     {  
  30.         printf("The operation is pending\n");  
  31.     }  
  32.     bRead = ReadFile(hDevice,buffer,10,&ulRead,&overlap2);  
  33.     if (!bRead && GetLastError()==ERROR_IO_PENDING)  
  34.     {  
  35.         printf("The operation is pending\n");  
  36.     }  
  37.       
  38.     //迫使程式中止2秒  
  39.     Sleep(2000);  
  40.   
  41.     //新建IRP_MJ_CLEANUP IRP  
  42.     CloseHandle(hDevice);  
  43.   
  44.     return 0;  
  45. }  

底下為執行結果 :


取消 IRP :
前面介紹了如何 "Pending" IRP 並將之插入佇列, 並且在關閉裝置時, 將 "Pending" 的 IRP 結束. 還有另一個方法可以將 "Pending" 的 IRP 逐個結束, 這就是取消 IRP 請求. 內核函式IoSetCancelRoutine 可以設置取消 IRP 請求的回呼函式, 其宣告如下 :
Syntax :
  1. PDRIVER_CANCEL IoSetCancelRoutine(  
  2.   __in  PIRP Irp,  
  3.   __in  PDRIVER_CANCEL CancelRoutine  
  4. );  

參數說明 :
* 參數 Irp : 這個參數是需要取消的 IRP.
* 參數 CancelRoutine : 這個式取消函式的函式指標. 一旦 IRP 請求被取消的時候, 作業系統會呼叫這個函式.

IoSetCancelRoutine 可以將一個取消常式與該 IRP 關聯, 一旦取消 IRP 請求時, 這個常式就會被執行. IoSetCancelRoutine 函式也可以用來刪除取消常式, 當輸入的 CancelRoutine 參數為空指標時, 則刪除原來設置的取消常式.
程式設計師可以用 IoCancelIrp 函式指定取消 IRP 請求. 在 IoCancelIrp 內部, 需要進行同步. DDK 在 IoCancelIrp 內使用一個叫做 cancel 的自旋鎖進行同步. IoCancelIrp 在內部會首先獲得該自旋鎖, IoCancelIrp 會呼叫取消常式, 因此釋放該自旋鎖的任務就留給了取消常式. 獲得取消自旋鎖的函式是 IoAcquireCancelSpinLock 函式, 而釋放取消自旋鎖的函式是IoReleaseCancelSpinLock 函式.
在應用程式中, 可以呼叫 CancelIo Win32 API 函式取消 IRP 請求. 在 CancelIo 的內部會枚舉所有沒有被完成的 IRP, 然後依次呼叫 IoCancelIrp. 另外如果應用程式沒有呼叫 CancelIo 函式, 應用程式在關閉裝置時同樣會自動呼叫 CancelIo. 下面程式示範如何編寫取消常式 :
  1. VOID CancelReadIRP(  
  2.     IN PDEVICE_OBJECT DeviceObject,  
  3.     IN PIRP Irp  
  4.     )  
  5. {  
  6.     KdPrint(("Enter CancelReadIRP\n"));  
  7.   
  8.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)  
  9.             DeviceObject->DeviceExtension;  
  10.       
  11.     //設置完成狀態為STATUS_CANCELLED  
  12.     Irp->IoStatus.Status = STATUS_CANCELLED;  
  13.     Irp->IoStatus.Information = 0;   // bytes xfered  
  14.     IoCompleteRequest( Irp, IO_NO_INCREMENT );  
  15.   
  16.     //釋放Cancel自旋鎖  
  17.     IoReleaseCancelSpinLock(Irp->CancelIrql);  
  18.   
  19.     KdPrint(("Leave CancelReadIRP\n"));  
  20. }  
  21.   
  22. NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)   
  23. {  
  24.     KdPrint(("Enter HelloDDKRead\n"));  
  25.   
  26.     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)  
  27.             pDevObj->DeviceExtension;  
  28.   
  29.     IoSetCancelRoutine(pIrp,CancelReadIRP);  
  30.   
  31.     //將IRP設置為掛起  
  32.     IoMarkIrpPending(pIrp);  
  33.   
  34.     KdPrint(("Leave HelloDDKRead\n"));  
  35.   
  36.     //返回pending狀態  
  37.     return STATUS_PENDING;  
  38. }  
下面我們在應用程式中呼叫 CancelIO Win32 API. 這樣可以導致 IRP 的取消常式被呼叫 :
- main.cpp : 函式 CancelIO 示範
  1. #include   
  2. #include   
  3.   
  4. int main()  
  5. {  
  6.     HANDLE hDevice =   
  7.         CreateFile("\\\\.\\HelloDDK",  
  8.                     GENERIC_READ | GENERIC_WRITE,  
  9.                     0,  
  10.                     NULL,  
  11.                     OPEN_EXISTING,  
  12.                     FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//森揭扢离FILE_FLAG_OVERLAPPED  
  13.                     NULL );  
  14.   
  15.     if (hDevice == INVALID_HANDLE_VALUE)  
  16.     {  
  17.         printf("Open Device failed!");  
  18.         return 1;  
  19.     }  
  20.   
  21.     OVERLAPPED overlap1={0};  
  22.     OVERLAPPED overlap2={0};  
  23.   
  24.     UCHAR buffer[10];  
  25.     ULONG ulRead;  
  26.       
  27.     BOOL bRead = ReadFile(hDevice,buffer,10,&ulRead,&overlap1);  
  28.     if (!bRead && GetLastError()==ERROR_IO_PENDING)  
  29.     {  
  30.         printf("The operation is pending\n");  
  31.     }  
  32.     bRead = ReadFile(hDevice,buffer,10,&ulRead,&overlap2);  
  33.     if (!bRead && GetLastError()==ERROR_IO_PENDING)  
  34.     {  
  35.         printf("The operation is pending\n");  
  36.     }  
  37.       
  38.     //迫使程式中止2秒  
  39.     Sleep(2000);  
  40.   
  41.     //顯式的呼叫CancelIo,其實在關閉裝置時會自動執行CancelIo  
  42.     CancelIo(hDevice);  
  43.   
  44.     //新建IRP_MJ_CLEANUP IRP  
  45.     CloseHandle(hDevice);  
  46.   
  47.     return 0;  
  48. }  

在取消常式中要注意同步問題是當退出取消常式, 一定要釋放 cancel 自旋鎖, 否則會導致系統崩潰. 另外 cancel 自旋鎖是全域自旋鎖, 所有驅動程式都會使用這個自旋鎖, 因此占用自旋鎖時間不宜過長. 底下是執行結果 :
This message was edited 18 times. Last update was at 17/01/2011 10:31:19

1 則留言:

網誌存檔

關於我自己

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