程式扎記: [ Windows DDP ] IRP 的同步 : 應用程式對裝置的同步/非同步作業

標籤

2011年2月14日 星期一

[ Windows DDP ] IRP 的同步 : 應用程式對裝置的同步/非同步作業

前言 : 
操作裝置的 Win32 API 主要是三個函式 ReadFile 函式, WriteFile 函式 和 DeviceIOControl 函式. 以 DeviceIoControl 函式為例, 它的同步操作如下圖所示 : 
 
當應用程式呼叫 DeviceIoControl 函式時, 它的內部會新建一個 IRP_MJ_DEVICE_CONTROL 類型的 IRP, 並將這個 IRP 傳送到驅動程式的派遣函式中. 處理該 IRP 需要一段時間, 值到 IRP 處理完畢後, DeviceIoControl 函式才會返回. 
同步操作時, 在 DeviceIoControl 的內部, 會呼叫 WaitForSingleObject 函式去等待一個事件, 這個事件直到 IRP 被結束的時候才會被觸發. 如果透果反組譯 IoCompleteRequest 內核函式, 就會發現在 IoCompleteRequest 內部設置了該事件. DeviceIoControl 會暫時進入睡眠狀態, 直到 IRP 被結束. 
而在非同步作業的情況下, 當 DeviceIoControl 被呼叫時, 其內部會產生 IRP 並將該 IRP 傳遞給驅動程式內部的派遣函式. 但此時 DeviceIoControl 函式不會等待該 IRP 結束, 而是直接返回. 當 IRP 經過一段時間結束後, 作業系統會觸發一個 IRP 相關事件. 這個事件可以通知應用程式 IRP 請求被執行完畢. 
下面舉個例子來比較同步與非同步作業的區別. 例如 DeviceIoControl 請求驅動程式去執行格式化磁碟的操作, 這個操作假設為十分鐘. 如果 DeviceIoControl 採用同步操作時, DeviceIoControl 函式從被呼叫到返回之間需要十分鐘時間. 因為 DeviceIoControl 中途會一直等待這個操作完成. 
如果 DeviceIoControl 函式採用非同步作業時, 執行 DeviceIoControl 的時間可能會忽略不計, 應用程式的執行緒可以先執行別的操作. 但這並不意味 DeviceIoControl 的操作已經完畢, 十分鐘後作業系統會透過一個事件通知應用程式, 剛才的 DeviceIoControl 操作才真正完畢. 
可以看出非同步作業比同步作業操作的效率高, 其實對於整個 Windows 作業系統而言, 它就是一個完全支援非同步的作業系統. 非同步作業系統比同步操作在性能上優越了許多, 但從程式設計的角度看, 非同步作業比同步操作複雜些. 
Win32 API 的非同步作業還必須得到驅動程式的支援, 如果驅動程式不支援非同步作業, Win32 API 的非同步作業將會失敗. 

同步操作裝置 : 
如果需要同步操作裝置, 在打開裝置的時候就要指定以 "同步" 的方式打開裝置. 打開裝置用 CreateFile 函式, 其宣告如下 : 

- Syntax :
  1. HANDLE WINAPI CreateFile(  
  2.   __in      LPCTSTR lpFileName,  // 裝置名  
  3.   __in      DWORD dwDesiredAccess,  // 存取權限  
  4.   __in      DWORD dwShareMode,  // 共用模式  
  5.   __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全屬性  
  6.   __in      DWORD dwCreationDisposition,  // 如何新建  
  7.   __in      DWORD dwFlagsAndAttributes,  // 裝置屬性  
  8.   __in_opt  HANDLE hTemplateFile  // 檔範本  
  9. );  

CreateFile 函式在前面已經多次用到, 其中第六個參數 dwFlagsAndAttributes 是同步非同步作業的關鍵. 如果這個參數中沒有設置 FILE_FLAG_OVERLAPPED, 則以後對裝置的操作都是同步操作, 否則所有操作為非同步作業. 對裝置操作的 Win32 API 例如 ReadFile, WriteFile 和 DeviceIoControl 函式都會提供一個 OVERLAP 參數, 如 ReadFile 函式 : 
- Syntax :
  1. BOOL WINAPI ReadFile(  
  2.   __in         HANDLE hFile,  
  3.   __out        LPVOID lpBuffer,  
  4.   __in         DWORD nNumberOfBytesToRead,  
  5.   __out_opt    LPDWORD lpNumberOfBytesRead,  
  6.   __inout_opt  LPOVERLAPPED lpOverlapped  
  7. );  

在同步操作裝置時, 其 lpOverlapped 參數 (也就是 OVERLAP 結構指標) 設置為 NULL. 下面的程式碼演示了應用程式如何對裝置進行 "同步" 讀取. 這裡操作檔代替操作裝置. 檔可以看做是廣義上的裝置 : 
- main.cpp :
  1. #include   
  2. #include   
  3.   
  4. #define BUFFER_SIZE 512  
  5. int main()  
  6. {  
  7.     HANDLE hDevice =   
  8.         CreateFile("test.dat",  
  9.                     GENERIC_READ | GENERIC_WRITE,  
  10.                     0,  
  11.                     NULL,  
  12.                     OPEN_EXISTING,  
  13.                     FILE_ATTRIBUTE_NORMAL,//此處沒有設置FILE_FLAG_OVERLAPPED  
  14.                     NULL );  
  15.   
  16.     if (hDevice == INVALID_HANDLE_VALUE)   
  17.     {  
  18.         printf("Read Error\n");  
  19.         return 1;  
  20.     }  
  21.   
  22.     UCHAR buffer[BUFFER_SIZE];  
  23.     DWORD dwRead;  
  24.     ReadFile(hDevice,buffer,BUFFER_SIZE,&dwRead,NULL);//這裡沒有設置OVERLAP參數  
  25.   
  26.     CloseHandle(hDevice);  
  27.   
  28.     return 0;  
  29. }  

非同步作業裝置 (方式一) : 
非同步作業裝置時主要需要設置 OVERLAP 參數, Windows 中用一種資料結構 OVERLAPPED 表示 : 
- Syntax :
  1. typedef struct _OVERLAPPED {  
  2.   ULONG_PTR Internal;  
  3.   ULONG_PTR InternalHigh;  
  4.   union {  
  5.     struct {  
  6.       DWORD Offset;  
  7.       DWORD OffsetHigh;  
  8.     } ;  
  9.     PVOID  Pointer;  
  10.   } ;  
  11.   HANDLE    hEvent;  
  12. } OVERLAPPED, *LPOVERLAPPED;  

參數說明 : 
* 參數 Offset : 操作裝置會指定一個偏移量, 從裝置的偏移量進行讀取. 該偏移量用一個 64 位元整數表示, Offset 就是偏移量的低 32 位整數.
* 參數 OffsetHight : OffsetHight 是偏移量的高 32 位元整數.
* 參數 hEvent : 這個事件用於操作完成後通知應用程式. 程式設計師可以初始化該事件為未激發, 當操作結束後, 即在驅動中呼叫 IoCompleteRequest 後, 該裝置被設置為激發

在使用 OVERLAPPED 結構前, 要先對其內部清零, 並且為其新建事件. 下面程式碼演示如何在應用程式中使用非同步作業 : 
- main.cpp :
  1. #include   
  2. #include   
  3.   
  4. #define BUFFER_SIZE 512  
  5. //假設該檔大於或等於BUFFER_SIZE  
  6.   
  7. #define DEVICE_NAME "test.dat"  
  8. int main()  
  9. {  
  10.     HANDLE hDevice =   
  11.         CreateFile("test.dat",  
  12.                     GENERIC_READ | GENERIC_WRITE,  
  13.                     0,  
  14.                     NULL,  
  15.                     OPEN_EXISTING,  
  16.                     FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此處設置FILE_FLAG_OVERLAPPED  
  17.                     NULL );  
  18.   
  19.     if (hDevice == INVALID_HANDLE_VALUE)   
  20.     {  
  21.         printf("Read Error\n");  
  22.         return 1;  
  23.     }  
  24.   
  25.     UCHAR buffer[BUFFER_SIZE];  
  26.     DWORD dwRead;  
  27.   
  28.     //初始化overlap使其內部全部為零  
  29.     OVERLAPPED overlap={0};  
  30.   
  31.     //新建overlap事件  
  32.     overlap.hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);  
  33.   
  34.     //這裡沒有設置OVERLAP參數,因此是非同步作業  
  35.     ReadFile(hDevice,buffer,BUFFER_SIZE,&dwRead,&overlap);  
  36.   
  37.     //做一些其他操作,這些操作會與讀設備並存執行  
  38.   
  39.     //等待讀裝置結束  
  40.     WaitForSingleObject(overlap.hEvent,INFINITE);  
  41.   
  42.     CloseHandle(hDevice);  
  43.   
  44.     return 0;  
  45. }  

非同步作業裝置 (方式二) : 
除了 ReadFile 和 WriteFile 函式外, 還有兩個 API 也可以實現非同步讀寫, 這就是 ReadFileEx 和 WriteFileEx 函式. ReadFile 和 WriteFile 既可以支援同步讀寫, 又可以支援非同步讀寫操作. 而 ReadFileEx 和 WriteFileEx 函式是專門用於非同步讀寫操作的, 不能進行同步讀寫. 首先看一下 ReadFileEx 的宣告 : 
- Syntax :
  1. BOOL WINAPI ReadFileEx(  
  2.   __in       HANDLE hFile,  // 裝置空制碼  
  3.   __out_opt  LPVOID lpBuffer,  // 讀取的緩衝區  
  4.   __in       DWORD nNumberOfBytesToRead,  // 讀取的位元組數  
  5.   __inout    LPOVERLAPPED lpOverlapped,  //  offset 指標  
  6.   __in_opt   LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine  // 完成函式  
  7. );  

參數說明 : 
* 參數 hFile : 要操作的裝置控制碼
* 參數 lpBuffer : 讀入資料的緩衝區
* 參數 nNumberOfBytesToRead : 需要讀取的位元組數
* 參數 lpOverlapped : 一個 OVERLAPPED 指標.
* 參數 lpCompletionRoutine : 完成常式

需要注意的是, 這裡提供的 OVERLAPPED 不需要提供事件控制碼. ReadFileEx 將請求傳遞到驅動程式後立刻返回. 驅動程式在結束讀取操作後, 會透過呼叫 ReadFileEx 提供的回呼函式 (Callback Function). 類似一個軟插斷, 也就是當讀取操作結束後, 系統立刻回呼 ReadFileEx 提供的回呼常式. Windows 將這種機制稱為非同步程序呼叫 (APC Asynchronous Procedure Call). 
然而 APC 的回呼函式被呼叫是有條件的. 只有執行緒處於 Alert 狀態時, 回呼函式才有可能被呼叫. 有多個 API 可以使系統進入 Alert 狀態, 如 SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectEx 函式等. 這些 Win32 API 都會有一個 BOOL 型的參數 bAlertable 當設置為 TRUE 時, 就進入 Alert 模式. 當系統進入 Alert 模式, 作業系統會枚舉當前執行緒的 APC 佇列. 驅動程式一旦結束讀取操作, 就會把 ReadFileEx 提供的完成常式插入到 APC 佇列. 
回呼函式會報告本此操作的完成狀況, 比如是成功或是失敗. 同是會報告本次讀取操作實際讀取的位元組數. 下面是一般回呼常式的宣告 : 
  1. VOID CALLBACK FileIOCompletionRoutine {  
  2.     DWORD dwErrorCode,  
  3.     DWORD dwNumberOfBytesTransfered,  
  4.     LPOVERLAPPED  lpOverlapped  
  5. };  
參數說明 : 
* 參數 dwErrorCode : 如果有讀取錯誤, 會返回錯誤碼.
* 參數 dwNumberOfBytesTransfered : 返回實際讀取操作的位元組數
* 參數 lpOverlapped : OVERLAP 參數, 指定讀取的偏移量等資訊.

下面範例演示了如何在應用程式中 ReadFileEx 進行非同步讀取操作 : 
- main.cpp :
  1. #include   
  2. #include   
  3.   
  4. #define DEVICE_NAME "test.dat"  
  5. #define BUFFER_SIZE 512  
  6. //假設該檔大於或等於BUFFER_SIZE  
  7.   
  8. VOID CALLBACK MyFileIOCompletionRoutine(  
  9.   DWORD dwErrorCode,                // 對於此次操作返回的狀態  
  10.   DWORD dwNumberOfBytesTransfered,  // 告訴已經操作了多少位元組,也就是在IRP裡的Information  
  11.   LPOVERLAPPED lpOverlapped         // 這個資料結構  
  12. )  
  13. {  
  14.     printf("IO operation end!\n");  
  15. }  
  16.   
  17. int main()  
  18. {  
  19.     HANDLE hDevice =   
  20.         CreateFile("test.dat",  
  21.                     GENERIC_READ | GENERIC_WRITE,  
  22.                     0,  
  23.                     NULL,  
  24.                     OPEN_EXISTING,  
  25.                     FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此處設置FILE_FLAG_OVERLAPPED  
  26.                     NULL );  
  27.   
  28.     if (hDevice == INVALID_HANDLE_VALUE)   
  29.     {  
  30.         printf("Read Error\n");  
  31.         return 1;  
  32.     }  
  33.   
  34.     UCHAR buffer[BUFFER_SIZE];  
  35.   
  36.     //初始化overlap使其內部全部為零  
  37.     //不用初始化事件!!  
  38.     OVERLAPPED overlap={0};  
  39.   
  40.     //這裡沒有設置OVERLAP參數,因此是非同步作業  
  41.     ReadFileEx(hDevice, buffer, BUFFER_SIZE,&overlap,MyFileIOCompletionRoutine);  
  42.   
  43.     //做一些其他操作,這些操作會與讀設備並存執行  
  44.   
  45.     //進入alterable  
  46.     SleepEx(0,TRUE);  
  47.   
  48.     CloseHandle(hDevice);  
  49.   
  50.     return 0;  
  51. }  

4 則留言:

  1. 作者已經移除這則留言。

    回覆刪除
    回覆
    1. 感謝回覆, 如果有延深閱讀需求, 文章來自 Windows Device Driver Programming驅動程式設計.
      http://www.books.com.tw/exep/prod/booksfile.php?item=0010439379

      刪除
    2. 謝謝~順便問一下 這本適合給想踏入寫Driver的人看嗎?
      還有 作者我點你的Device Driver Programming (41) 頁面最上面的那個按鈕
      然後點----> 顯示具有 Device Driver Programming 標籤的文章。 顯示所有文章 <-----
      似乎並沒有列出一份清單, 列出有關於Device Driver Programming的文章
      而是要按較舊的文章, 感覺41篇文章這樣一直按 挺不方便的@@
      還是說是瀏覽器的問題...(Firefox)

      刪除
    3. 因為這涉及版權問題, 所以如果你真的有心朝 Device Driver Programming 發展, 買本工具書是很有幫助的^^. 這裡是我閱讀後方便日後參考的整裡, 至於你提到需要按 "較舊文章" 來在不同主題間切換不方便的話, 可以參考下面網址有做 index :
      http://puremonkey2010.blogspot.com/2012/03/windows-ddp-index.html
      最後看完這本書的心得是真的很適合當新手入門的踏板, 但真正的價值在於你遇到書上沒有提到的問題而解決, 這才是真的成長. ^^

      刪除

網誌存檔

關於我自己

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