非同步裝置I/O適用於大資料量和高效能的場合,比如伺服器。
要使用非同步裝置I/O,在調用CreateFile來開啟或建立一個裝置的時候,讓參數dwFlagsAndAttributes包括FILE_FALG_OVERLAPPED,這意味著想讓開啟的裝置可以被非同步訪問。
為了發送一個I/O請求給一個裝置,也就是讓一個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。當使用非同步I/O方式的時候,在調用這兩個函數的時候,可以將NULL傳遞給pdwNumBytes參數,因為不知道何時裝置I/O完成,因此使用這個參數沒有多大意義。
注意最後一個參數,是一個OVERLAPPED結構的指標:
typedef struct _OVERLAPPED {
DWORD Internal; // 錯誤碼(出口參數,返回)
DWORD InternalHigh; // 傳輸的資料大小,以位元組為單位(出口參數,返回)
DWORD Offset; // 低32位位移量(入口參數,輸入)
DWORD OffsetHigh; // 高32位位移量(入口參數,輸入)
HANDLE hEvent; // 事件核心物件控點(入口參數,輸入)
} OVERLAPPED, *LPOVERLAPPED;
該結構包含5個成員,其中的3個——Offset、OffsetHigh、hEvent應該在調用ReadFile和WriteFile之前被初始化,另外的2個——Internal、InternalHigh會在I/O完成的時候被裝置驅動程式所設定,下面細述一下:
- Offset、OffsetHigh —— 在使用非同步裝置I/O來操縱“檔案裝置”的時候,檔案讀寫指標被忽略,此時I/O的位移量由OVERLAPPED結構中的Offset和OffsetHigh決定。另外,在“非檔案裝置”中,這兩個成員不會被忽略,一般必須要設定為0。
- hEvent —— 一個事件核心物件控點,可以有多種使用方法,後面會講到。
- Internal —— 儲存I/O錯誤碼,當你發送一個I/O請求的時候,該參數被設定為STATUS_PENDING,指明沒有錯誤發生,因為操作還沒有開始。你可以使用HasOverlappedIoCompleted宏來查看一個非同步裝置I/O是否完成,該結構接受一個OVERLAPPED結構指標,如果I/O請求完成返回TRUE。如果I/O請求仍然沒有開始,返回FALSE。
- InternalHigh —— 非同步I/O請求完成的時候,該成員裡儲存了傳送資料量的位元組數。
當非同步I/O請求完成之後,你可以接受到一個OVERLAPPED結構的指標。一般可以讓一個C++類從OVERLAPPED結構派生,類中加入一些其他資訊,使得更容易處理。然後當使用ReadFile和WriteFile函數的時候,可以傳遞這個C++類對象的指標,當I/O完成之後,接受該結構的時候,可以將其轉換為C++類對象,不但可以獲得其5個成員,還可以獲得類中的其他資訊。
使用非同步裝置I/O的時候,要注意以下三點:
1、裝置驅動程式不一定會按照一個“先進先出”(FIFO)的順序來處理裝置I/O請求,因此如下編碼不會保證先讀後寫:
OVERLAPPED o1 = { 0 };
OVERLAPPED o2 = { 0 };
BYTE bBuffer[100];
ReadFile (hFile, bBuffer, 100, NULL, &o1); //讀
WriteFile(hFile, bBuffer, 100, NULL, &o2); //寫
2、以非同步方式進行I/O請求的是,驅動程式可能會選擇同步的方式。當你讀取一個檔案的時候,如果系統發現讀取的資料在cache中,且資料有效,那麼該I/O請求就不需要驅動程式了,而是直接將cache中的資料複製到你的緩衝區中。驅動在某些操作上一直使用同步方式,比如在NTFS格式上的檔案壓縮,擴充檔案長度,添加檔案資訊等。
這個時候,如果ReadFile和WriteFile返回非0值,則表明它以同步方式進行。如果返回FLASE,說明發生了一個錯誤,這個時候可以通過GetLastError來取得資訊,如果返回ERROR_IO_PENDING,則說明I/O請求成功提交,但沒有完成。
3、資料緩衝區和OVERLAPPED結構在非同步I/O請求完成之前不能被移動或釋放。當裝置驅動準備處理你的I/O請求的時候,它將資料傳送到pvBuffer參數對應的地址上去,並訪問OVERLAPPED結構中的Offset等成員。當I/O請求完成之後,裝置驅動更新OVERLAPPED結果中的Internal和InternalHigh成員。因此,不能在I/O請求完成之前移動或釋放資料緩衝區和OVERLAPPED結構,否則,記憶體資料會被破壞,而且在每次調用ReadFile或WriteFile的時候,都必須分配一個單獨的OVERLAPPED結構。
比如,下面的代碼是有BUG的:
VOID ReadData(HANDLE hFile)
{
OVERLAPPED o = { 0 };
BYTE b[100];
ReadFile(hFile, b, 100, NULL, &o);
} //此時緩衝區b和OVERLAPPED結構o都被釋放
你可以將一個裝置I/O請求清除佇列,即撤消該請求。可以有如下方法:
1、在一個線程中調用CancelIo函數,可以取消該線程發送給指定裝置有關的所有I/O請求,除了指定的裝置是“I/O完成連接埠”。
BOOL CancelIo(HANDLE hFile); //參數是裝置物件控點
2、取消與一個裝置有關的所有I/O請求,關閉這個裝置控制代碼即可。
3、當一個線程結束,系統自動取消該線程發送的所有I/O請求,除了發送給“I/O完成連接埠的”I/O請求。
4、如果想取消某一個特定的I/O請求,可以使用CancelIoEx函數,傳遞一個OVERLAPPED結構指標給它:
BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);
該函數可以跨線程使用,也就是在T1線程內發送的I/O請求,可以在T2線程內通過該函數結束之。因為每個I/O請求都需要一個唯一的OVERLAPPED結構,所以該OVERLAPPED結構就標識了一個I/O請求。如果傳遞NULL給CancelIoEx函數的第2個參數,那麼就會取消與hFile對應的裝置的所有I/O請求。
取消一個I/O請求,該I/O請求會結束,同時錯誤碼被設定為ERROR_OPERATION_ABORTED。