揭开重叠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的心得体会,对它做了一个简单的介绍,讲的比较粗浅,如果有的地方有错误,也请及时告诉我,以便我改正错误!


你可能感兴趣的:(Win32,IO,异步,OVERLAPPED)