WINDOWS下異步IO

 
網上介紹驅動程序的異步I/O和事件通知的教程實在太難找了,大多都是一筆帶過,有的也只是給出一個基本框架,以偶的水平,打死我也寫不出一個完整的代碼出來 武安河的《windows 2000/xp WDM 設備驅動程序開發》一書中有這一部分的內容,不過是用DriverStudio以類的方式講解的,實在難懂。最後終於在《windows WDM設備驅動程序開發指南》中發現其中第14章的DebugPrint源代碼講的就是這部分內容,我對驅動層代碼進行了註釋,自己寫了一個簡單的用戶態程序,終於完成了驅動專題的WDM部分,呵呵 
下面的理論部分爲轉載,上述代碼見附件,困,快半夜3點了,不知能加精否,應該有苦勞的 
一.基本框架(不知哪位老大翻譯的):
代碼:
 
HANDLE hfile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bBuffer[100];
OVERLAPPED o = { 0 };
o.Offset = 345; 
BOOL fReadDone = ReadFile(hfile, bBuffer, 100, NULL, &o);
DWORD dwError = GetLastError();
if (!fReadDone && (dwError == ERROR_IO_PENDING))
{   // The I/O is being performed asynchronously; wait for it to complete   
    WaitForSingleObject(hfile, INFINITE);
    fReadDone = TRUE;
}
if (fReadDone){   
// o.Internal contains the I/O error   
// o.InternalHigh contains the number of bytes transferred   
// bBuffer contains the read data} 
else{   
// An error occurred; see dwError
}
一旦線程發出一個異步I/O請求後,就可以繼續執行,做其它有用的任務。最終,線程仍需要和該I/O操作的完成同步換句話說,你的線程運行至某個地方就得停下來,直到來自設備的數據已完整地載入緩衝區,線程代碼才能繼續執行下去。
在Windows中,一個設備內核對象可用於線程同步,故內核對象要麼處於有信號態要麼處於無信號態。ReadFile和WriteFile函數在排隊一個I/O請求前,即把設備內核對象設置爲無信號態。當設備驅動程序完成請求後,驅動程序把設備內核對象設置爲有信號態。
線程可以調用WaitForSingleObject或WaitForMultipleObjects來判斷一個異步I/O請求是否已完成。下面是一個簡單的例子:
這段代碼發出一個異步I/O請求,隨後立即等待請求完成,實際上背離了異步I/O的目的。顯然,你絕不會編寫類似的代碼,不過這些代碼卻演示了一些重要的規則,我總結如下:
■ 執行異步I/O的設備必須用FILE_FLAG_OVERLAPPED標誌打開。
■ OVERLAPPED結構的Offset,OffsetHigh和hEvent成員必須被初始化。在示例代碼中,除了Offset我把它們全設置爲0,Offset設爲345使ReadFile從文件開始第346個字節處讀數據。
■ ReadFile的返回值保存於fReadDone中,該值指示了I/O請求是否被同步執行。
■ 如果I/O請求不是被同步執行,我就檢查是否產生一個錯誤或是I/O被異步執行。通過將GetLastError返回值和ERROR_IO_PENDING比較,即可對此得出結論。
■ 爲了等待數據,我調用WaitForSingleObject並傳遞設備內核對象的句柄。WaitForSingleObject會掛起調用線程直至內核對象變成有信號態。設備驅動在它完成I/O後使內核對象有信號。在WaitForSingleObject返回後,I/O被完成,我把fReadDone設爲TRUE。
■ 在讀完成後,你可以檢查bBuffer中的數據,錯誤碼存放在OVERLAPPED結構的Internal成員裏,傳輸字節數存放在OVERLAPPED結構的InternalHigh成員裏。
■ 如果確實發生了錯誤,dwError存放的錯誤碼給出了進一步的信息。

二.異步設備I/O基礎(by 馬伕 2006-03-25 11:50:44  ):

與計算機執行的大多數操作相比,設備I/O是其中速度最慢和結果最難以預料的。CPU在執行算術運算甚至畫屏幕時也比從一個文件或網絡上讀寫數據要快得多。不過,使用異步設備I/O可讓你更好地利用資源並創建更有效的程序。
設想線程向設備發出了一個異步I/O請求的情況:I/O請求被傳遞到實際進行I/O的設備驅動程序,當驅動程序在等待設備響應時,線程不是掛起等待I/O請求完成,而是繼續執行其它有用的任務。
關鍵之處在於,設備驅動程序要處理完排隊的I/O請求,並且必須通知程序已經發送或接收了數據,或產生了錯誤。你將在下一節,“接收I/O請求完成通知”,學習設備驅動程序如何通知你I/O完成了。現在,讓我們集中考慮如何排隊異步I/O請求。排隊異步I/O請求是設計高性能、可伸縮的程序的精髓,也是本章所有內容的沉澱。
要異步地訪問一個設備,你就必須在調用CreateFile打開該設備時,在dwFlagsAndAttrs參數中指定FILE_FLAG_OVERLAPPED標誌。這個標誌告訴系統你想要異步地訪問設備。
要給設備驅動程序排隊一個I/O請求,你可使用在“進行同步設備I/O”一節中已學過的ReadFile和WriteFile函數。爲了閱讀方便,我這裏再次列出函數原型:
代碼:
BOOL ReadFile(   HANDLE      hfile,    PVOID       pvBuffer,   DWORD       nNumBytesToRead,    PDWORD      pdwNumBytes,   OVERLAPPED* pOverlapped);
BOOL WriteFile(   HANDLE      hfile,    CONST VOID  *pvBuffer,   DWORD       nNumBytesToWrite,    PDWORD      pdwNumBytes,   OVERLAPPED* pOverlapped);
    當這兩個函數中的一個被調用時,函數會檢查由hfile參數標識的設備是否用FILE_ FLAG_OVERLAPPED標誌打開。若指定了該標誌,函數就執行異步I/O。順便說一句,在用這兩個函數進行異步I/O時,你可以(也是通常的做法)傳遞NULL給pdwNumBytes參數。既然你期望這些函數在I/O完成前就返回,那麼檢查此刻已傳輸的字節數就是毫無意義的。
OVERLAPPED結構 


    要進行異步設備I/O,你必須通過pOverlapped參數傳遞一個已初始化的OVERLAPPED結構的地址。此處的“overlapped”一詞意味着花費在執行I/O請求上的時間與線程花費在執行其它任務上的時間是重疊的。OVERLAPPED結構看起來如下面所示:

代碼:
typedef struct _OVERLAPPED {   DWORD  Internal;     // [out] Error code   DWORD  InternalHigh; // [out] Number of bytes transferred   DWORD  Offset;       // [in]  Low  32-bit file offset   DWORD  OffsetHigh;   // [in]  High 32-bit file offset   HANDLE hEvent;       // [in]  Event handle or data} OVERLAPPED, *LPOVERLAPPED;
 
    該結構裏包含5個成員。其中的三個:Offset、OffsetHigh和hEvent,必須在調用ReadFile和WriteFile前被初始化。另外兩個成員:Internal和InternalHigh,由設備驅動程序設置且可以在I/O操作完成後查看。下面是這些成員變量的更詳細的解釋:


    ■ Offset和OffsetHigh 在訪問文件時,這兩個成員指示了在文件中的64-bit偏移值,也就是你想執行I/O操作的起始位置。前面提到每個文件內核對象都關聯有一個文件指針,當發出一個同步I/O請求時,系統知道在文件指針標識的位置開始訪問文件。在一次同步操作完成後,系統自動更新文件指針以便下一次操作可以從本次操作的結束位置繼續。


    當執行異步I/O時,文件指針被系統忽略。想象一下會發生什麼,如果你的代碼(對同一個文件內核對象)先後執行了兩次緊鄰的異步ReadFile調用。在這種情況下,系統無法知道第二次調用ReadFile時要從哪裏開始讀。你可能並不想從第一次調用ReadFile讀文件的起始位置執行第二次讀操作,而是想從第一次調用ReadFile讀出的最末一個字節之後開始執行第二次讀操作。爲了避免對同一個對象的多次異步調用發生混亂,所有的異步文件I/O請求必須在OVERLAPPED結構中指定起始偏移值。


    注意,Offset和OffsetHigh成員並不會被非文件設備忽略你必須把這兩個成員都初始化爲0,否則I/O請求將失敗,且GetLastError會返回ERROR_INVALID_PARAMETER。


    ■ hEvent 接收I/O完成通知的方法有四種,其中一種使用了這個成員。當使用告警I/O通知方法時,這個成員則可以按你自己的方式使用。我知道很多開發者把C++對象的地址存放在hEvent中(在“使事件內核對象有信號”一節中我們將進一步討論這個成員)。


    ■ Internal 這個成員保存被執行的I/O的錯誤碼。一旦你發出一個異步I/O請求,設備驅動就設置Internal爲STATUS_PENDING,表示沒有錯誤出現,只是操作還沒有開始。事實上,定義在WinBase.h中的宏HasOverlappedIoCompleted,允許你檢查I/O操作是否已經完成。如果請求仍然是未決的,它返回FALSE;如果I/O請求已完成,它返回TRUE。下面是該宏的定義:

代碼:
#define HasOverlappedIoCompleted(pOverlapped)    ((pOverlapped)->Internal != STATUS_PENDING)
 
    ■ InternalHigh 當一個異步I/O請求完成後,這個成員保存已傳輸的字節數。


    在最初設計OVERLAPPED結構時,Microsoft決定不公佈Internal和InternalHigh成員(這就是它們名字的來歷)。隨着時間過去,Microsoft意識到包含在這些成員中的信息對開發者是有用的,就把它們歸檔了。然而Microsoft沒有改變這些成員的名稱,因爲操作系統的源代碼經常引用它們,而Microsoft不想修改這些代碼。

異步設備I/O告誡

    在進行異步I/O時,你應該明白幾個要點。第一,設備驅動程序不會以先進先出的的方式處理排隊的I/O。舉個例子,如果線程執行下面的代碼,設備驅動程序很可能先寫文件然後再去讀文件:

代碼:
OVERLAPPED o1 = { 0 };
OVERLAPPED o2 = { 0 };
BYTE bBuffer[100];
ReadFile (hfile, bBuffer, 100, NULL, &o1);
WriteFile(hfile, bBuffer, 100, NULL, &o2);
    只要有助於提高性能,設備驅動程序會特地打亂次序來執行I/O請求。比如,爲了減少磁頭的移動和尋道時間,文件系統驅動程序會搜查排隊I/O請求的列表,尋找那些相鄰於硬盤上同一物理位置的請求。


    第二個要點是你要知道進行錯誤檢查的正確方式。大多數Windows函數返回FALSE來表示失敗或返回非零來表示成功。然而,ReadFile和WriteFile函數的行爲有點不同。一個例子程序有助於說明這點。


    在試圖排隊一個異步I/O請求時,設備驅動程序可能決定以同步方式來處理這個請求。當你讀一個文件而系統要檢查你需要的數據是否已在系統緩存中時,就會出現這種情況:如果數據可以(從緩存)獲得,你的I/O請求不會被設備驅動排隊,系統改爲從緩存中複製數據到你的緩衝區,完成該I/O操作。


    如果請求的I/O被同步執行完,ReadFile和WriteFile返回一個非零值。如果請求的I/O被異步執行,或在調用ReadFile和WriteFile時發生錯誤,都返回FALSE。當返回FALSE時,你必須調用GetLastError以確切地判斷髮生了什麼。如果GetLastError返回ERROR_IO_PENDING,則I/O請求已被成功地排隊並將隨後完成。


    如果GetLastError返回值不是ERROR_IO_PENDING,則I/O請求可能未被設備驅動排隊。下面是I/O請求可能未被設備驅動排隊時,GetLastError返回的最常見的錯誤碼:


        ■  ERROR_INVALID_USER_BUFFER或ERROR_NOT_ENOUGH_MEMORY 每個設備驅動維護有一個長度固定的未決的I/O請求列表(在非分頁內存池內)。如果這個表滿了,系統就不能排隊你的請求,ReadFile和WriteFile返回FALSE,而GetLastError則報告這兩個錯誤碼之一(取決於驅動程序)。


        ■  ERROR_NOT_ENOUGH_QUOTA 一些設備要求你的數據緩衝區內存頁面鎖定,使得在I/O未決期間,數據不會被切換出RAM。這種頁面鎖定的內存要求,當然適合於使用FILE_FLAG_NO_BUFFERING標誌進行文件I/O。但是,系統制約了單個進程可以頁面鎖定的內存容量。如果ReadFile和WriteFile不能頁面鎖定你的緩衝區,它們就返回FALSE,而GetLastError報告ERROR_NOT_ENOUGH_QUOTA。你可以通過調用SetProcessWorkingSetSize來增加一個進程的(物理內存)配額。


    你應該怎樣處理這些錯誤呢?基本上,這些錯誤的產生都是因爲有許多的未決I/O請求來不及完成,所以你得允許一些未決I/O請求執行完畢,然後再發起對ReadFile和WriteFile的調用。


    第三個你得知道的要點是,用於執行異步I/O請求的數據緩衝區和OVERLAPPED結構,必須在I/O請求完成後才能被移走或銷燬。當給一個設備驅動排隊一個I/O請求時,傳遞給驅動的是數據緩衝區的地址和OVERLAPPED結構的地址。注意,被傳遞的只是地址,而不是實際的數據塊。這樣做的原因是顯然的:內存來回複製是代價昂貴的並且浪費了大量的CPU時間。


    當設備驅動即將處理你排隊的請求時,它傳遞由pvBuffer地址引用的數據,並訪問文件的偏移量成員和包含在由pOverlapped參數指定的OVERLAPPED結構中的其它成員。特別地,設備驅動還要用I/O的錯誤碼更新Internal成員,用已傳輸的字節數更新InternalHigh成員。

注意
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 

直至I/O請求被完成後才能移動或銷燬這些緩衝區是絕對必要的,否則會導致內存混亂。同樣地,你必須爲每一個I/O請求分配和初始化一個獨立的OVERLAPPED結構。 

    開發者對於上述“注意”之處要引起高度重視,它也是在實現一個異步I/O架構時易犯的最常見錯誤。這裏有一個處理不當的例子:

VOID ReadData(HANDLE hfile) {   OVERLAPPED o = { 0 };   BYTE b[100];   ReadFile(hfile, b, 100, NULL, &o);}
 


    這段代碼看起來毫無問題,對ReadFile的調用是正確的。唯一的問題在於函數在排隊了一個異步I/O請求後就返回了。函數的返回實質上釋放了源於線程堆棧的緩衝區和OVERLAPPED結構,而設備驅動並不知道ReadData返回了。設備驅動仍舊有兩個指向線程堆棧的地址。當I/O完成時,設備驅動要修改在線程堆棧上的內存,任何碰巧在此時佔用了該內存位置的數據將被損壞。這種錯誤特別難於查找因爲內存改變是異步發生的。有時設備驅動可能同步執行I/O,這種情況下你不會發現錯誤。有時I/O可能在函數返回後完成,甚至超過一個小時後完成,那麼誰能知道此刻堆棧正在被用作什麼呢?


取消已排隊的設備I/O請求

有時你可能想要在設備驅動處理完一個已排隊的設備I/O請求前取消它,Windows對此提供了一些方法:
調用CancelIo取消在調用線程中排隊並指定同一個的句柄的所有的I/O請求:
    BOOL CancelIo(HANDLE hfile);
 關閉設備句柄,取消所有已排隊的I/O請求,無論這個請求由哪個線程排隊。
 在線程消亡時,系統會自動取消該線程發出的所有I/O請求。
你可以看出,沒有方法能取消一個單獨的、指定的I/O請求。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章