Windows核心編程學習筆記(21)--同步設備I/O與異步設備I/O2

Drecik學習經驗分享

轉載請註明出處:http://blog.csdn.net/drecik__/article/details/8186961


1. 異步設備I/O基礎

異步設備I/O是指,當一個線程向設備發送一個異步I/O請求時,這個I/O被傳送給設備驅動程序,後者負責完成實際的I/O慚怍。當驅動程序在等待設備響應的時候,應用程序的線程並沒有因爲要等待I/O請求完成而被掛起,線程會繼續運行並執行其他有用的任務。

通俗點來說,如果是異步調用處理設備I/O的函數,該函數會立即返回,不用等待。

使用異步I/O的操作方式:

  1. 在CreateFile創建設備對象的時候在dwFlagsAndAttributes中指定FILE_FLAG_OVERLAPPED標誌
  2. 在ReadFile和WriteFile中指定最後一個LPOVERLAPPED 參數

1. 介紹OVERLAPPED結構

typedef struct _OVERLAPPED {
	ULONG_PTR Internal;				// 返回出錯誤碼;
	ULONG_PTR InternalHigh;			// 返回傳輸的字節;
	union {
		struct {
			DWORD Offset;			// 指定文件訪問中應該從哪裏開始訪問文件數據;
			DWORD OffsetHigh;
		} DUMMYSTRUCTNAME;
		PVOID Pointer;
	} DUMMYUNIONNAME;

	HANDLE  hEvent;					// 用來通知請求完成;
} OVERLAPPED, *LPOVERLAPPED;
其中第一個參數Internal保存已處理的I/O請求的錯誤碼,一旦發送異步I/O請求,設備驅動程序會立即將該參數設備STATUS_PENDING,表示沒有錯誤,應用程序可以使用HasOverlappedIoCompleted宏來檢查一個異步I/O操作是否已經完成。

2. 異步I/O的注意事項

我們需要注意以下幾點:

1. 設備驅動程序不必以先入先出的方式處理隊列中的I/O請求,即不一定是先調用I/O請求的就一定先完成

2. 檢查錯誤的方式,使用異步調用ReadFile或WriteFile都會立即返回,返回爲FALSE,GetLastError檢查是ERROR_IO_PENDING說明I/O操作正在進行並且沒發生錯誤,如果GetLastError返回其他值說明發送錯誤調用失敗

3. 在異步操作請求完成之前,一定不能移動或銷燬I/O請求使用的數據緩存和OVERLAPPED結構,每個I/O請求都必須分配和初始化一個OVERLAPPED結構

3. 取消隊列中的設備I/O請求

我們肯呢個想要在設備驅動程序對一個已經在加入隊列的設備I/O請求進行處理之前取消:

1. 調用CancelIo來取消給定句柄所標示的線程天極道隊列中的所有I/O請求(除非該句柄具有與之相關聯的I/O完成端口)

2. 關閉設備句柄來取消已經添加到隊列中的所有I/O請求,不管它們是由哪個線程創建

3. 線程終止時候,系統會自動取消該線程發出的所有I/O請求,關聯I/O完成端口的請求是例外

4. 使用CancelIoEx取消某個設備的某個異步I/O請求,有兩個參數,第一個爲設備句柄,第二個爲指定的OVERLAPPED對象,用來指定取消哪個異步I/O請求,如果第二個參數爲NULL,則取消該設備所有的I/O請求

2. 接收I/O請求完成通知

Windows提供4中不同方法來接收I/O請求已經完成的通知

1. 觸發設備內核對象

當一個線程觸發一個設備異步I/O請求時,設備內核對象就會設爲未觸發狀態,當設備驅動程序完成了請求之後,會將設備內核對象設爲觸發狀態。

所以線程可以通過WaitForSingleObject或WaitForMultipleObjects。

這種方法使用簡單,但是缺點是每個內核對象每次只能處理一個異步I/O請求,否則的話不知道是哪個請求完成。

2. 觸發事件內核對象

發送異步I/O請求,傳入的OVERLAPPED結構最後一個成員hEvent用來標識一個事件內核對象,我們必須通過CreateEvent來創建你該對象。

當一個異步I/O請求完成的時候,設備驅動程序會檢查hEvent成員是否爲NULL,不爲NULL則觸發該事件,所以我們可以等待每個異步I/O請求的hEvent事件是否觸發來判斷是否完成請求。

多個異步設備I/O請求必須爲每個請求創建不同的事件對象。

我們可以使用GetOverlappedResult來獲得異步I/O的結果:

BOOL
GetOverlappedResult(
	HANDLE hFile,						// 設備句柄;
	LPOVERLAPPED lpOverlapped,			// 異步I/O請求時傳入的OVERLAPPED結構體;
	LPDWORD lpNumberOfBytesTransferred,	// 傳輸的字節;
	 BOOL bWait							// 是否等待請求完成;
	);

我們也可以自己等待hEvent成員觸發,然後自己檢查OVERLAPPED的Internal來判斷錯誤碼,InternalHigh判斷傳輸的字節。

爲了略微提高性能,我們可以使用函數SetFileCompletionNotificationModes設置不觸發第一種情況下所說的設備內核對象:

BOOL
SetFileCompletionNotificationModes(
	HANDLE FileHandle,	// 設備句柄;
	UCHAR Flags			// 標誌,傳入FILE_SKIP_SET_EVENT_ON_HANDLE;
	);

 

3. 可提醒I/O

首先需要知道的是,可提醒I/O設計的非常糟糕,所以應該避免使用!

每個線程創建的時候都會創建一個異步過程調用隊列,當發出一個I/O請求的時候,我們可以告訴設備驅動程序在調用線程的APC隊列中添加一項。

爲了能夠實現我們的調用ReadFileEx和WriteFileEx函數來完成異步I/O請求,該兩個函數和原先函數相比,少了返回已經傳輸的字節數,多了最後一個LPOVERLAPPED_COMPLETION_ROUTINE參數,該參數是一個回調函數,函數形式如下:

VOID
(WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
	DWORD dwErrorCode,					// 錯誤碼;
	DWORD dwNumberOfBytesTransfered,	// 實際傳輸的字節;
	LPOVERLAPPED lpOverlapped			// 異步請求時候的OVERLAPPED結構體;
	);

使用ReadFileEx和WriteFIleEx發出的請求會將回調函數地址傳給設備驅動程序,當請求完成的時候,會向發出請求的線程的APC隊列中添加一項。

順便提下,可提醒I/O完成時不會觸發OVERLAPPED的hEvent成員,所以可以佔爲己用。

當線程處於可提醒狀態,系統會檢查它的APC隊列,對隊列中的每一項,都會調用完成函數並傳入參數。

當I/O請求完成時候,系統將他們會添加到APC隊列中——並不會立即調用。線程爲了對APC隊列的項進行處理,必須將自己置爲可提醒狀態。Windows提供了6個函數可將線程置爲可提醒狀態:SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectsEx,SingalObjectAndWait,GetQueuedCompletionStatusEx,MsgWaitForMultipleObjectsEx。前5個參數最後一個參數是個BOOL值,表示是否將線程置爲可提醒狀態,最後一個函數的最後一個參數必須使用標誌MWMO_ALERTABLE來讓線程進入可提醒狀態。

當使用這些函數調用等待函數並設置可提醒狀態時,如果APC隊列有項,則他們並不會被掛起。

可提醒I/O的劣勢:

  1. 可提醒必須創建一個回調函數,是代碼變得複雜,因爲我們不得不把換掉函數用到的信息放在全局變量中。
  2. 線程問題,發出I/O請求的線程必須同時對完成通知進行處理,伸縮性不是很好。

可提醒I/O的優點:

Windows提供函數QueueUserAPC手動向將一項添加到APC隊列中:

DWORD
QueueUserAPC(
	PAPCFUNC pfnAPC,	// 回調函數;
	HANDLE hThread,		// 線程句柄;
	ULONG_PTR dwData	// 傳入回調函數的參數;
	);

回調函數指針原型:

VOID
(NTAPI *PAPCFUNC)(
	ULONG_PTR Parameter	// QueueUserAPC的第3個參數;
	);

hThread標識的線程可以在另一個進程的地址空間中,如果是那樣那麼pfnAPC也必須在同一個進程的地址空間中。

QueueUserAPC可以用來強制讓線程退出等待狀態:

如果某一個線程正在等待,並且該線程是可提醒的,我們可以向它APC隊列添加項來使它根據等待函數返回的內容來判斷是否退出,處理APC隊列的等待函數返回的是WAIT_IO_COMPLETION

 

4. I/O完成端口

完成端口是到目前爲止伸縮性最好的處理I/O請求的方法,他可以讓用戶創建等待請求線程,然後在另外一個線程完成請求,適合多個併發操作的請求,例如套接字請求。

這裏指介紹完成端口的使用方法:

1. 瞭解CreateIoCompletionPort函數:

HANDLE		// 返回創建的句柄;
CreateIoCompletionPort(
	HANDLE FileHandle,				// 設備句柄;
	HANDLE ExistingCompletionPort,	// 當前存在的完成端口句柄,用於與設備句柄關聯;
	ULONG_PTR CompletionKey,		// 關鍵字,通常是一個結構體指針,標識I/O的請求,該指針會在結果中返回;
	DWORD NumberOfConcurrentThreads	// 允許併發線程數目,通常爲0表示等於CPU數目;
	);
使用該函數最常用的是,先創建一個完成端口,然後與設備進行關聯:
// 創建完成端口;
HANDLE hComplete = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );

// 與設備句柄關聯;
CreateIoCompletionPort( hFile, hComplete, dwKey, 0 );
也可以在創建的時候直接關聯:
// 創建完成端口的時候直接關聯;
HANDLE hComplete = CreateIoCompletionPort( hFile, NULL, dwKey, 0 );
如果我們的設備已經與完成端口關聯,但是我們發出一個的請求,使它在完成時候不添加到I/O完成端口隊列中,則我們需要設置發送請求的OVERLAPPED的hEvent成員:
// 創建OVERLAPPED對象,並讓hEvent與按位1或起來;
OVERLAPPED ol = {0};
ol.hEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
ol.hEvent = (HANDLE)((DWORD_PTR)ol.hEvent | 1 );

ReadFile(..., &ol);

// 關閉事件的時候不要忘了將低位清除;
CloseHandle( (HANDLE)((DWORD_PTR)ol.hEvent & ~1) );
另外提一個,如果關聯了完成端口的設備,在發送同步請求的時候,請求完成Windows也會將結果放到完成隊列中,爲了略微提高性能,可能使用上面提到的SetFileCompletionNotificationModes函數,第一個參數傳入設備句柄,第二個參數傳入FILE_SKIP_COMPLETION_PORT_ON_SUCCESS告訴Windows不要將以同步方式完成的請求放到完成端口中。

2. 獲得I/O請求的結果

通常處理I/O請求結果是在另外一個獨立的工作線程,且該線程的數量最好是處理器數量的2倍左右。

完成端口將完成的I/O請求添加到I/O完成隊列中,工作線程可以使用函數GetQueuedCompletionStatus來從完成端口獲取到完成的I/O請求:

BOOL
GetQueuedCompletionStatus(
	HANDLE CompletionPort,				// 完成端口句柄;
	LPDWORD lpNumberOfBytesTransferred,	// 實際傳送的字節數;
	PULONG_PTR lpCompletionKey,			// 設備句柄與完成端口關聯的時候傳入的關鍵字;
	LPOVERLAPPED *lpOverlapped,			// 發送I/O請求時候的OVERLAPPED結構體指針;
	DWORD dwMilliseconds				// 等待時間,可以爲INFINITE;
	);
也可以使用函數GetQueuedCompletionStatusEx來同時獲取多個I/O請求結果:
BOOL
GetQueuedCompletionStatusEx(
	HANDLE CompletionPort,						// 完成端口句柄;
	LPOVERLAPPED_ENTRY lpCompletionPortEntries,	// OVERLAPPED_ENTRY數組指針;
	ULONG ulCount,								// 上面數組的數量;
	PULONG ulNumEntriesRemoved,		// 實際獲得的OVERLAPPED_ENTRY數組元素個數;
	DWORD dwMilliseconds,			// 等待時間;
	BOOL fAlertable					// 是否進入可提醒狀態;
	);

// OVERLAPPED_ENTRY結構體;
typedef struct _OVERLAPPED_ENTRY {
	ULONG_PTR lpCompletionKey;		// 關鍵字;
	LPOVERLAPPED lpOverlapped;		// 返回發送I/O請求時候的OVERLAPPED結構體的指針;
	ULONG_PTR Internal;				// 沒有明確定義,不應該使用;
	DWORD dwNumberOfBytesTransferred;	// 實際傳輸的字節數;
} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY;


3. 爲了方便理解,我拿了以前一本Socket書上的基於完成端口的實現代碼,可以去下載看看:http://download.csdn.net/detail/drecik__/4776851

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章