揭開重疊IO的神秘面紗

0x00  --- 引言

在Windows平台下對文件、外設、管道等IO操作都是通過WIN32的ReadFile、WriteRead函數進行的。
最常用的就是直接讀取或寫入,完成後返回實際寫入、讀取的字節大小。

//假設文件句柄hFile存在並有效
LPVOID readBuf[BUF_SIZE] = {0}; //讀取緩沖
DWORD  real_read = 0; //實際讀取的字節數
ReadFile(hFile,readBuf,READ_SIZE,&real_read,NULL);

下文皆用"文件"泛指文件、設備、管道等IO對象。
如果采用這種方式讀"文件",意味著線程會有阻塞,而且當一個"文件",句柄在被ReadFile占用阻塞時,另一處使用WriteRead也會阻塞等待ReadFile完成。反之亦然。
微軟提供給開發者更高級的操作"文件"IO的方式

有重疊IO模型(overlapped I/O)
完成端口模型(IOCP)


這裏主要講解重疊IO模型,它是完成端口模型的基礎。
還要說明一下,有些人可能了解一些相關內容,會覺得疑惑,這不是在WINSOCK相關的內容嗎?
其實WINSOCK上的重疊IO與完成端口都是由"文件"的基礎上演變的。
Windows上的SOCKET也可以用操作文件的ReadFile和WriteFile來收發數據,因爲SOCKET本質是一個"文件"句柄。

0x01 --- 創建一個文件吧

既然是講的是文件操作,那麽假設您已經了解一些Windows的文件IO的函數,我就不啰嗦的講解CreateFile的每個參數和用法,不了解的童鞋可以自行翻閱相關資料。

HANDLE CreateFile(
  LPCTSTR lpFileName, //指向文件名的指針
  DWORD dwDesiredAccess, //訪問模式(寫/讀)
  DWORD dwShareMode, //共享模式
  LPSECURITY_ATTRIBUTES lpSecurityAttributes, //指向安全屬性的指針
  DWORD dwCreationDisposition, //如何創建
  DWORD dwFlagsAndAttributes, //文件屬性
  HANDLE hTemplateFile //用于複制文件句柄
);

要說明的是只有當dwFlagsAndAttributes參數含有FILE_FLAG_OVERLAPPED標志時代表這個"文件"進行重疊IO的操作

hFile = CreateFile(_T("TESTFILE"), 
          GENERIC_READ | GENERIC_WRITE, 
          0, 
          NULL, 
          OPEN_EXISTING, 
          FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 
          NULL);

相關錯誤處理這裏就不做了,本文著重介紹概念。下同

重疊IO模型中有三種異步通知操作可供選擇。它們分別利用文件對象,重疊結構事件對象與APC回調。

0x02 --- 老朋友ReadFile和WriteFile

BOOL ReadFile(
HANDLE          hFile,//文件的句柄
LPVOID          lpBuffer,//用于保存讀入數據的一個緩沖區
DWORD           nNumberOfBytesToRead,//要讀入的字節數
LPDWORD         lpNumberOfBytesRead,//指向實際讀取字節數的指針
LPOVERLAPPED    lpOverlapped
);
 
BOOL WriteFile(
HANDLE             hFile,//文件句柄
LPCVOID            lpBuffer,//數據緩存區指針
DWORD              nNumberOfBytesToWrite,//你要寫的字節數
LPDWORD            lpNumberOfBytesWritten,//用于保存實際寫入字節數的存儲區域的指針
LPOVERLAPPED       lpOverlapped//OVERLAPPED結構體指針
);

前4個參數相信你一定都知道它們的含義,最後一個參數往往在普通IO操作時都是NULL,現在我們就要使用它了。

這裏需要傳入一個OVERLAPPED結構體的指針。

typedef struct _OVERLAPPED
{
  DWORD Internal; 
  DWORD InternalHigh; 
  DWORD Offset; 
  DWORD OffsetHigh; 
  HANDLE hEvent; 
} OVERLAPPED;

這個結構體是重疊IO模型的核心,稱之爲重疊結構

Internal              如果IO發生了錯誤 那麽在這個成員中保存錯誤碼
InternalHigh      完成時這個成員是已經操作的字節數
Offset                文件數據的偏移低32位(4字節)
OffsetHigh        文件數據的偏移高32位(4字節)
hEvent               事件對象句柄


在調用ReadFile、WriteFile時需要先行把OVERLAPPED的訪問"文件"位置偏移設置好
其中"文件"對象通知與重疊結構事件對象通知的讀取操作都一樣,只是完成通知方式不同。
這裏采用WriteFile舉例,因爲ReadFile當然可以使用重疊IO方式讀取,但目前電腦效能強大,
經過多次實驗都是很快能夠讀到記憶體緩沖中,不會發生阻塞,故而用WriteFile。

if( WriteFile(hFile,buf,WRITE_SIZE,&real_write,&overlap) )
{
    printf("無需重疊I/O方式,寫入已完成,實際寫入%d字節\n",real_write);
}
else
{
    error_code = GetLastError();
    if(error_code == ERROR_IO_PENDING)
    {
        printf("overlapped操作被放到隊列中等待執行\n");
    }
    else
    {
        printf("ERRCODE:%d\n",error_code);
    }
}

需要關注WriteFile的返回值,如果是TRUE代表系統很快就可以完成寫入文件操作不需要再用重疊IO的方式通知。
當返回值是FALSE的時候就需要GetLastError()獲取錯誤碼 ,
但在這裏當錯誤碼是ERROR_IO_PENDING不代表錯誤,而是代表系統已經接收了重疊IO的方式異步寫入文件。


0x03 --- 完成通知

1.利用文件對象通知

在重疊IO操作被成功提交後,直到操作完成前,系統會將"文件"句柄置爲無信號狀態,這意味著可以用wait系列函數去等待。

如:

DWORD WINAPI WaitForSingleObject(__in HANDLE hHandle, __in DWORD dwMilliseconds);

既然"文件"內核對象在執行重疊IO操作時處于無信號狀態,那麽線上程中調用WaitForSingleObject(hFile,INFINITE);

在文件寫入完成之前都是阻塞的,但我們可以利用第二個參數合理的設置超時時間,
這裏需要用到WaitForSingleObject的返回值。

當返回值是WAIT_OBJECT_0時意味著等待的內核對象有信號

當返回值是WAIT_TIMEOUT時代表是超時完成

//WriteFile返回值判別完成後,接上面的代碼塊
//重疊IO ---> 阻塞 
WaitForSingleObject(hFile,INFINITE);//一直等到完成
 
//重疊IO ---> 非阻塞
DWORD ret = WaitForSingleObject(hFile,TIME_OUT);
if(ret == WAIT_OBJECT_0)
{
    //完成 處理 
}
else if(ret == WAIT_TIMEOUT) 
{
    //超時時間到
    //其他一些操作 
}


WAIT_OBJECT_0 表示是事件已經有信號,並不代表重疊IO成功!

需要使用GetOverlappedResult函數獲取重疊IO操作狀態的信息

BOOL GetOverlappedResult(
        HANDLE hFile,  
        LPOVERLAPPED lpOverlapped,  //入口參數
        LPDWORD lpNumberOfBytesTransferred,  //出口參數
        BOOL bWait 
);

第三個參數lpNumberOfBytesTransferred是一個出口參數,返回實際操作的字節數
記得看過一個資料說其實GetOverlappedResult就是對第二個參數的重疊結構解析得到實際操作字節數,
還記得之前提到過OVERLAPPED的InternalHigh成員嗎?

最後一個參數bWait

當bWait是TRUE時 調用後它會阻塞 直到IO操作完成後返回,等價于使用WaitForSingleObject(hFile,INFINITE);

當bWait是FALSE時,調用後會立即返回,如果返回TRUE代表已經操作完成,
如果是FALSE,通過GetLastError()獲取到錯誤碼爲ERROR_IO_INCOMPLETE代表操作未完成,而不是發生了錯誤。

//重疊IO ---> 阻塞 
GetOverlappedResult(hFile,&overlap,&real_write,TRUE);

//重疊IO ---> 非阻塞
BOOL ret = GetOverlappedResult(hFile,&overlap,&real_write,FALSE);
if(ret)
{
        printf("寫入完成\n");
       //完成 處理  
}
else
{
 error_code = GetLastError();
 if(error_code == ERROR_IO_INCOMPLETE)
 {
  printf("尚未寫入完成\n");
 }
 else
 {
  printf("發生錯誤\n");
 }
}

談一下應用場景
假如在一個工作線程在循環內不停輪詢,收到一個消息通知我們使用重疊IO方式去寫入文件,
執行完WriteRead後需要等到完成通知去進行一個提示操作,但線上程內還有別的事情要處理。

假如上面的代碼在一個線程的循環裏執行采用不阻塞的方式,通過返回值判斷,如果沒完成就可以進行一些其他的操作。

有人說重疊IO就是文件異步IO,其實這並不能劃上等號。

重疊IO只是在ReadFile和WriteFile上不阻塞,而後面由程序員靈活的控制自己需要的方式,但這兩種阻塞是不同的,
在ReadFile和WriteFile阻塞IO操作是 阻塞是因爲在進行讀寫的IO操作,而使用文件對象通知的重疊IO阻塞等待是在等在句柄對應的內核對象是否有信號,他們的本質是不同的。
這裏涉及到中斷DMA的知識,不再展開。

阻塞 非阻塞  同步 異步 重疊IO  這些是不同的概念


2.利用重疊結構事件對象通知

假如在某個程序中有兩個線程共享一個文件句柄。

第一個線程需用重疊IO的方式打開文件寫入偏移爲0-1024字節的數據,
第二個線程也用重疊IO的方式打開文件寫入偏移爲2048-4096字節的數據,
倘若使用文件句柄的方式獲取通知,怎麽知道是哪個線程的操作完成了呢?顯然,使用文件句柄獲得完成通知的方式,在這個場景裏就顯得不好用了,這裏需要用到重疊結構的最後一個成員hEvent,它是一個事件對象的句柄。

在進行ReadFile與WriteFile之前,我們可以創建一個事件內核對象,
用OVERLAPPED的hEvent成員保存有效的句柄值,這裏需要強調的是這個事件對象必須是一個手動複位對象,且初始化爲無信號狀態,因爲在完成IO操作是操作系統會將這個事件對象置爲有信號狀態。

hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);//手動 初始無信號

if(hEvent != INVALID_HANDLE_VALUE)
{
 overlap.hEvent = hEvent;//針對每個操作使用不同的結構和事件
}

于是 我們針對每一個ReadFile或WriteFile關聯了不同的帶有事件對象成員重疊結構

在需要獲得完成通知時仍然利用wait系列函數去以阻塞或非阻塞超時的方式,根據返回值獲取完成通知,
只是將原本等待的文件句柄換成所關聯的重疊結構的事件內核對象句柄
完成後利用GetOverlappedResult通過第二個參數傳入重疊結構 獲取有效信息

ret = WaitForSingleObject(overlap.hEvent,TIME_OUT);
if(ret == WAIT_OBJECT_0)
{
 bRes = GetOverlappedResult(hFile,&overlap,&real_write,FALSE);
 if(bRes)
 {
  printf("寫入完成 實際寫%d字節\n",real_write);
  //完成 處理
 }
 else
 {
  error_code = GetLastError();
  if(error_code == ERROR_IO_INCOMPLETE)
  {
   printf("尚未寫入完成\n");
  }
  else
  {
   printf("發生錯誤\n");
  }
 }
}
else if(ret == WAIT_TIMEOUT) 
{
    //超時時間到
    //其他一些操作 
}

3.注冊異步(APC)回調獲得完成通知

除了ReadFile和WritrFile之外WIN32還提供了ReadFileEx與WriteFileEx兩個函數

BOOL WINAPI ReadFileEx(
HANDLE hFile, //文件的句柄
LPVOID lpBuffer, //用于接收數據的緩沖區
DWORD nNumberOfByteToRead, //允許接收的最大字節數
LPOVERLAPPED lpOverlapped, //一個OVERLAPPED結構的指針
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //異步讀取完成後調用的回調函數
)
 
BOOL WINAPI WriteFileEx(
HANDLE hFile, //文件的句柄
LPCVOID lpBuffer,//指定一個緩沖區,其中包含了要寫入的數據。除非寫操作完成,否則不要訪問這個緩沖區
DWORD nNumberOfBytesToWrite,//要寫入數據的字節量
LPOVERLAPPED pOverlapped,//一個OVERLAPPED結構的指針
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //回調函數指針

請注意這兩個函數的第3個參數nNumberOfByteToRead與nNumberOfBytesToWrite他們是傳遞兩個DWORD類型的值,並像ReadFile與WriteFile
中一樣是傳遞一個出口參數的指針用于返回操作的字節數。
因爲在這是異步回調的模式,將會在回調函數中獲取到實際操作的字節數。

最後一個參數LPOVERLAPPED_COMPLETION_ROUTINE函數指針的聲明:

來自MSDN

typedef VOID (*LPOVERLAPPED_COMPLETION_ROUTINE) (
        [in] DWORD  dwErrorCode,
        [in] DWORD  dwNumberOfBytesTransfered,
        [in] LPVOID lpOverlapped
);


dwErrorCode
[in] 一個值,如果設備已關閉,則此值爲錯誤代碼,否則此值爲零。
如果關閉設備,則會立即完成所有挂起的設備 I/O。

dwNumberOfBytesTransfered
[in] I/O 操作傳送的字節數。

lpOverlapped
[out] 一個指針,指向包含將用于完成 I/O 請求的信息的結構。

這裏有一個常用的使用技巧
由于在異步回調的方式進行重疊IO操作,我們不需要使用重疊結構的hEvent成員。
而hEvent是一個HANDLE,而HANDLE就是typedef void* HANDLE 本質就是一個指針類型,所以我們可以利用這個成員來傳遞一些自己有用數據的地址,
如果是在C++裏的話可以同這個成員保存一個this指針,在回調時強制類型轉換後進行相關操作。

char data[BUF_SIZE] = "DATA";//模擬要傳遞的數據
st_overlap.hEvent = (HANDLE)data;//通過事件對象句柄保存數據的指針

ret = WriteFileEx(hFile,write_buf,//寫入緩存
                  1024,//緩存大小
                  &st_overlap,//重疊結構指針
                  FileIOCompletionRoutine//完成回調函數
                  );


回調函數

VOID CALLBACK FileIOCompletionRoutine(
	_In_     DWORD dwErrorCode,
	_In_     DWORD dwNumberOfBytesTransfered,
	_Inout_  LPOVERLAPPED lpOverlapped)
{
	if(dwErrorCode == NO_ERROR)
	{
		printf("dwErrorCode:%lu \ndwNumberOfBytesTransfered:0x%X \nlpOverlapped:%p \n",dwErrorCode,dwNumberOfBytesTransfered,lpOverlapped);
	}
	printf("%s\n",(char*)lpOverlapped.hEvent);
}


在執行完ReadFile或WriteFile後,我們需要將當前線程進入"alertable"狀態

在這兒需要引入幾個API

DWORD WINAPI SleepEx(DWORD dwMilliseconds,BOOL bAlertable);
DWORD WINAPI WaitForMultipleObjectsEx(
  _In_  DWORD nCount,
  _In_  const HANDLE *lpHandles,
  _In_  BOOL bWaitAll,
  _In_  DWORD dwMilliseconds,
  _In_  BOOL bAlertable
);
DWORD WINAPI WaitForSingleObjectEx(
  _In_  HANDLE hHandle,
  _In_  DWORD dwMilliseconds,
  _In_  BOOL bAlertable
);


最常見的就以上三個函數,還有其他一些能使線程處于可警告狀態的函數,有興趣可以查看相關資料,發現有什麽共同點了嗎?沒錯,他們都有一個bAlertable成員。

引入一段MSDN的原文

  • If this parameter is TRUE and the thread is in the waiting state, the function returns when the system queues an I/O completion routine or APC, and the thread runs the routine or function. Otherwise, the function does not return and the completion routine or APC function is not executed.

    A completion routine is queued when the ReadFileEx or WriteFileEx function in which it was specified has completed. The wait function returns and the completion routine is called only if bAlertable is TRUE and the calling thread is the thread that initiated the read or write operation. An APC is queued when you call QueueUserAPC.

關于異步回調的中頻繁出現一個詞叫做APC(異步過程調用)

關于APC的涉及到WINNT內核的知識,這裏不做詳細的介紹,但有一點必須了解,APC是重疊IO異步回調這個機制得以運行的的基礎。


再看這幾個API,其他參數和非Ex版本的都類似,最後一個參數當它是TRUE時會在超時時間或者回調函數被調用時返回。

當回調函數被調用時返回WAIT_IO_COMPLETION

int callback_Overlapped()
{
	HANDLE		hFile;
	OVERLAPPED	st_overlap;
	BOOL		ret;
	char*		write_buf;
	DWORD		error_code;
	DWORD		apc_ret;

	hFile = CreateFile("TEST_FILE",	//文件名
			GENERIC_READ | GENERIC_WRITE,//訪問方式
			0,//共享模式
			NULL,//安全屬性
			OPEN_EXISTING,//創建描述
			FILE_FLAG_OVERLAPPED,//重疊選項
			NULL							
			);

	if(INVALID_HANDLE_VALUE == hFile)
	{
		fprintf(stderr,"The file open fail\n");
		return -1;
	}

	write_buf =  malloc( sizeof(BYTE) * 1024*1024 );
	if(!write_buf)
	{
		CloseHandle(hFile);
		fprintf(stderr,"Write buffer alloc fail\n");
		return -1;
	}

	memset(&st_overlap,0,sizeof(OVERLAPPED) );
	st_overlap.Offset = 0;//文件偏移
	st_overlap.hEvent = 0;//異步過程調用模式此參數可自定義(HANDLE就是void*)


	ret = ReadFileEx(hFile,//文件句柄
			write_buf,//寫入緩存
			1024*1024,//緩存大小
			&st_overlap,//重疊結構指針
			FileIOCompletionRoutine//完成回調函數
			);
	if(ret)
	{
		printf("overlapped操作被放到隊列中等待執行\n");
	}
	else
	{
		error_code = GetLastError();
		printf("error code:%d\n",error_code);
	}
        
WAIT_APC:
	apc_ret = WaitForSingleObjectEx(hFile,1,TRUE);
	
	switch (apc_ret)
	{
	case WAIT_ABANDONED:
		printf("擁有mutex的線程在結束時沒有釋放核心對象\n");
		break;
	case WAIT_IO_COMPLETION:
		printf("等待用戶模式APC隊列結束\n");
		break;
	case WAIT_OBJECT_0:
		printf("核心對象已被激活\n");
		break;
	case WAIT_TIMEOUT:
		printf("超時時間到\n");
		break;
	case WAIT_FAILED:
		printf("出現錯誤\n");
	}

	if(apc_ret != WAIT_IO_COMPLETION)
	{
		goto WAIT_APC;	
	}

	printf("WAIT_IO_COMPLETION");
	CloseHandle(hFile);
	free(write_buf);
	return EXIT_SUCCESS;
}


以上是我對重疊IO的心得體會,對它做了一個簡單的介紹,講的比較粗淺,如果有的地方有錯誤,也請及時告訴我,以便我改正錯誤!


更多相關文章
  • 我們期待了很久lambda爲java帶來閉包的概念,但是如果我們不在集合中使用它的話,就損失了很大價值.現有接口遷移成爲lambda風格的問題已經通過default methods解決了,在這篇文章將深入解析Java集合裏面的批量數據操作(bulk operation),解開lambda最強作用的神 ...
  • 1我們知道,在Linux設備驅動開發中,包括三大設備類:字符設備,塊設備和網路設備.而字符設備,作爲最簡單的設備類,爲此,我們將從最簡單的字符設備開始,走進Linux驅動程序設計的神秘殿堂.--我們已經踏上了真正的設備驅動開發的道路了!有志者,事竟成.付出越多,而上蒼定會以同等的收獲回饋于你,當然, ...
  • 四.     實現重疊模型的步驟作 了這麽多的准備工作,費了這麽多的筆墨,我們終于可以開始著手編碼了.其實慢慢的你就會明白,要想透析重疊結構的內部原理也許是要費點功夫,但是只是學會 如何來使用它,卻是真的不難,唯一需要理清思路的地方就是和大量的客戶端交互的情況下,我們得到事件通知以後,如何得知是哪一 ...
  • "身爲一個初學者,時常能體味到初學者入門的艱辛,所以總是想抽空作點什麽來盡我所能的幫助那些需要幫助的人.我也希望大家能把自己的所學和他人一起分享,不要去鄙視別人索取時的貪婪,因爲最應該被鄙視的是不肯付出時的吝啬."                                  ...
  • 安全健行4:揭開shellcode的神秘面紗
    2015/5/18 16:20:18 前面我們介紹了shellcode使用的基本策略,包括基本的shellcode.反向連接的shellcode以及查找套接字的shellcode.在宏觀上了解了shellcode之後,今天我們來深入一步,看看shellcode到底是什麽.也許大家和我一樣,從接觸安全 ...
  • 揭開opencart2.0神秘面紗2.0探索
    揭開opencart2.0神秘面紗(2.0探索)opencart2.0 從設計到現在也超過大半年了,一直沒有發布,萬人呼萬人喊也不露面,昨日我從github上下載最新開發程序,在本地測試了下,與大家交流下.總體感覺後台變化最大.先說安裝:=============================安裝 ...
  •       接上一篇的博文:如果每一個菜單切換的時候,都要隱藏其余所有的菜單,那就會導致代碼十分的臃腫,以前隱藏的代碼是這樣的: /*if (!openPositionFragment.isAdded()) { // 先判斷是否被add過 transaction.hide(priceFragment ...
  • GMF樹形布局 3 展開/折疊時更換Node圖標
    前一篇博客實現了展開/折疊,但是如果當節點折疊時圖標可以發生變化,例如變成加號,那就直觀了.這篇博客解決這個問題. 具體步驟如下: 1.首先,將兩個圖標文件放在diagram工程下的icons\custom下,並刷新這個工程,如下圖所示: 2.修改Topic節點圖標的地方,在TopicNameEdi ...
一周排行
  • 實戰-Ueditor擴展二次開發
    第一部分 開發前期准備   1.UEditor從1.4.1開始,添加對于二次開發的擴展支持 ...
  • 問題就不多說了,直接記錄正題, 主要涉及2個命令cvt和xrandr, 主要注意的一點是:剛剛開始直接用命令寫入,但是注銷或重啓後又還原了, 于是寫了個腳本,添加到開機啓動項裏,暫時先這樣吧 #!/bin/bash ...
  • Implicit conversion is  the scala ways to allow you to extend libraries code or in another word, code writte ...
  • 1,調用系統相機     <1>自己的Activity爲豎屏,調用系統相機,當返回的時候,自己的Activity會從橫屏切換成豎屏.求大神指教如何不讓自己的Activity出現從橫屏切換成豎屏.      ...
  • java中的gc log解讀
    gc log是java程序在出現記憶體問題時候最好的查看問題的有利日志.下面我們來一步一步 ...
  • Symbolic references in Java
  • |背景:    最近一個有個專案用到了EasyUI DataGrid,其中有個DataGrid載入較爲緩慢(>5s),這個列表用到了editor|測試:   第一步: 數據准備時間<1s,DataGrid ...
  • 俗話說:愛美之心,人皆有之.是的,沒錯,即使我只是一個做地圖的,我也希望自己的地圖看起來好看一點.在本文,給大家講講在第二種狀態,地圖拖拽時出現,此時,需要分別監聽map的mouse-drag-start和mouse ...
  • 1 固定集合 可以理解爲一個大小固定的隊列集合 新來者會把老的擠走! 固定集合不能分片,可以用于記錄日志, 雖然可以創建時指定集合大小,但是無法控制什麽時候數據會被覆蓋! 2 固定集合的創建 普通集合可以先不指定,但 ...
  • 1. 優化SQL   1)通過show status了解各種sql的執行頻率         show status like 'Com_%'        了解 Com_select,Com_insert 的執行次 ...