介绍

简单讲解下我们程序进行IO的过程,当线程进行一个同步的设备IO请求时,他会被挂起,直到设备完成IO请求,返回给阻塞线程,线程激活继续处理。当进行一个异步的设备IO请求时,该线程可以先去做其他事,等到设备完成IO请求后通知该线程进行处理。本文讨论在windows平台下的异步设备IO。同时在一些示例中会对涉及到的知识进行讲解。

1.异步IO执行

进行异步设备io时我们来做一下下准备工作,首先针对不同的设备(文件,管道,套接字,控制台)的初始化和发出IO不太一样,以简单的文件为例,别的应该都是相通的。

1.1 初始化设备(eg.CreateFile)

首先我们来说下在windows下他的api大多数有后缀为W和A两种情况,W表示以unicode(utf-16)字符编码,
A表示以ANSI字符编码,我们以W为例,然后我们使用CreateFile创建文件设备对象,CreateFile也可以用来创建目录,磁盘驱动器,串口,并口等设备对象。这里我们用最简单的文件为例。

WINBASEAPI
HANDLE
WINAPI
CreateFileW(_In_ LPCWSTR lpFileName,_In_ DWORD dwDesiredAccess,_In_ DWORD dwShareMode,_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,_In_ DWORD dwCreationDisposition,_In_ DWORD dwFlagsAndAttributes,_In_opt_ HANDLE hTemplateFile);
  • WINBASEAPI宏表示__declspec(dllimport)是用来导入导出时使用
  • HANDLE类型表示内核对象,比如线程,进程,事件,设备等,操作系统来维护的。
  • WINAPI 宏是__stdcall,VC编译器的指令,可以来设置传参的时入栈的参数顺序,栈内数据清除方式,函数签名等
  • lpFileName文件名
  • dwDesiredAccess访问方式,可读、可写等
  • dwShareMode,其他内核对象使用是的共享方式
  • lpSecurityAttributes 安全属性
  • dwCreationDisposition 打开方式,创建还是打开已有等

  • 我们如果使用CreateFile来进行异步IO,我们需要将dwFlagsAndAttributes设置带有FILE_FLAG_OVERLAPPED属性。OVERLAPPED重叠的意思,表示内核线程和应用线程重叠运行。

1.2 执行(eg.ReadFile,WriteFile)

WINBASEAPI
_Must_inspect_result_
BOOL
WINAPI
ReadFile(_In_ HANDLE hFile,_Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer,_In_ DWORD nNumberOfBytesToRead,_Out_opt_ LPDWORD lpNumberOfBytesRead,_Inout_opt_ LPOVERLAPPED lpOverlapped);WINBASEAPI
BOOL
WINAPI
WriteFile(_In_ HANDLE hFile,_In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,_In_ DWORD nNumberOfBytesToWrite,_Out_opt_ LPDWORD lpNumberOfBytesWritten,_Inout_opt_ LPOVERLAPPED lpOverlapped);

来看下ReadFile的解释

  • hFile即为上一节的设备对象
  • lpBuffer是文件最后读到的缓冲区,或者要写到设备的缓冲区
  • nNumberOfBytesToRead要读取多少字节,nNumberOfBytesToWrite要写多少字节
  • lpNumberOfBytesRead指向一个DWORD的地址,表示最终读取了多少字节,lpNumberOfBytesWritten最终写了多少字节。

然后就是lpOverlapped了,我们来看下LPOVERLAPPED的结构

typedef struct _OVERLAPPED {ULONG_PTR Internal;ULONG_PTR InternalHigh;union {struct {DWORD Offset;DWORD OffsetHigh;} DUMMYSTRUCTNAME;PVOID Pointer;} DUMMYUNIONNAME;HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;
  • Internal用来保存等到已经处理完IO后的错误码
  • InternalHigh用来保存已传输的字节数
  • Offset和InternalHigh构成一个64位的偏移值,表示访问文件从哪里开始访问
  • Pointer系统保留字
  • hEvent用来接收I/O完成通知时使用,后边会说到

2. IO请求完成通知

然后我们来看下,等到IO完成后如何通知到线程中,有四种方式来通知,摘自《windows核心编程》:

方法 描述
触发设备内核对象 允许一个线程发出IO请求,另一个线程对结果处理,只能同时发出一个IO请求
触发事件内核对象 允许一个线程发出IO请求,另一个线程对结果处理 ,能同时发出多个IO请求
可提醒I/O 只允许一个线程发出IO请求,须发出请求的线程对结果处理,能同时发出多个IO请求
I/O完成端口 循序一个线程发出IO请求,另一个线程对结果处理,能同时发出多个IO请求

2.1 触发设备内核对象

先来看例子:

int main()
{HANDLE hFile = CreateFile(L"1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);if (hFile == INVALID_HANDLE_VALUE) {std::cout << "open error";return -1;}BYTE bBuffer[1024];OVERLAPPED o = { 0 };BOOL bReadDone = ReadFile(hFile, bBuffer, 1024, NULL, &o);DWORD dwError = GetLastError();if (!bReadDone && dwError == ERROR_IO_PENDING) {DWORD dw = WaitForSingleObject(hFile, INFINITE);bReadDone = TRUE;}if (bReadDone) {std::cout << o.Internal << std::endl;std::cout << o.InternalHigh << std::endl;bBuffer[o.InternalHigh] = '\0';std::cout << bBuffer << std::endl;}else {std::cout << "read error";return 0;}std::cout << "succ";return 0;
}

CreateFile用可读可写的权限;用OPEN_ALWAYS的打开方式,表示有文件打开,没有该文件创建文件。
这个例子对一些判断比较完整,我们可以顺便来巩固下基础知识,CreateFile成功返回句柄,失败时返回INVALID_HANDLE_VALUE,而不是像许多windows返回句柄为NULL来表示失败了,但是CreateFile失败返回的是INVALID_HANDLE_VALUE(-1),大家可以注意下。
然后进行初始化,声明的BYTE数组来存放读取到的数据;OVERLAPPED 对象初始化为0,即中的元素值都是0,这里要注意的是Offset为0即为从文件的开头读取数据。
调用ReadFile后,由于是异步的,所以bReadDone 是FALSE,然后获取下错误信息,得知是ERROR_IO_PENDING,表示正在进行IO操作。
最后我们调用WaitForSingleObject(hFile, INFINITE)来等待hFile设备内核对象触发,这里我们大概讲解下关于内核对象触发。

在windows中,内核对象可以用来进行线程同步,内核对象有两个状态:触发和,未触发。比如说线程,进程,他们在创建时是未触发的,运行结束时变为触发状态。在比如Event对象,可以我们写代码来使他的程序变化,后边我们再说。
这里我们说下文件内核对象,ReadFile和WriteFile函数在将IO请求添加到设备的队列之前,会先将状态设为未触发状态,当设备驱动程序完成了所谓请求后,会将对象状态设为触发状态。
再来说WaitForSingleObject函数,就是等待第一个参数(内核对象句柄)状态变成触发,等待时间是第二个参数,等待该时间后或者内核对象状态变成触发该函数返回。

我们先往文件中写入“01234567899876543210”
最后我们打印出来读取结果,依次打印出错误码,读取的字节数,读取内容。另外我们首先在文件中写入了内容。

这个有一个缺点就是,只能同时处理一个IO请求。

2.2 触发事件内核对象

继续看例子:

static bool readReady = false;
void WaitResultThd(void *param)
{HANDLE* hh = (HANDLE*)param;DWORD dw = WaitForMultipleObjects(2, hh, TRUE, INFINITE);if (dw == WAIT_OBJECT_0) {readReady = true;}
}int main()
{HANDLE hFile = CreateFile(L"1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);if (hFile == INVALID_HANDLE_VALUE) {std::cout << "open error";return -1;}BYTE bBuffer1[11] = {0};OVERLAPPED o1 = { 0 };o1.hEvent = CreateEvent(NULL, FALSE, FALSE, L"");o1.Offset = 0;ReadFile(hFile, bBuffer1, 10, NULL, &o1);BYTE bBuffer2[11] = { 0 };OVERLAPPED o2 = { 0 };o2.hEvent = CreateEvent(NULL, FALSE, FALSE, L"");o2.Offset = 10;ReadFile(hFile, bBuffer2, 10, NULL, &o2);HANDLE h[2];h[0] = o1.hEvent;h[1] = o2.hEvent;_beginthread(WaitResultThd, 0, h);while (1){/* do somthing*/Sleep(500);if (readReady) {std::cout << bBuffer1 << std::endl;std::cout << bBuffer2 << std::endl;break;}}return 0;
}

我们看下这个和上一个的区别是用OVERLAPPED的hEvent变量来实现IO完成的通知,首先CreateEvent为每个OVERLAPPED的变量创建事件内核对象,看下CreateEvent:

WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateEventW(_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,_In_ BOOL bManualReset,_In_ BOOL bInitialState,_In_opt_ LPCWSTR lpName);
  • lpEventAttributes设置的安全属性
  • bManualReset,意为是否为手动重置对象,为TRUE表示手动重置,事件触发时正在等待改事件的所有线程将都变成可调度状态。为FALSE为自动重置,事件触发时只有一个线程变成可调度状态。
  • bInitialState初始状态,TRUE是触发状态,FALSE为未触发状态
  • lpName是可以用次来共享该事件对象

当我们创建成功了时间内核对象时,可以使用SetEvent将其设置为触发状态,可以使用ResetEvent将其设置为未触发状态

我们继续,当异步IO请求完成后,设备驱动程序会检查OVERLAPPED的hEvent是不是为空,如果不是为空,调用SetEvent来触发该对象。
为了演示可以多线程来进行操作,我们开启另一个线程来等待事件完成,使用WaitForMultipleObjects来等待多个事件触发,我们再来看下WaitForMultipleObjects

WINBASEAPI
DWORD
WINAPI
WaitForMultipleObjects(_In_ DWORD nCount,_In_reads_(nCount) CONST HANDLE* lpHandles,_In_ BOOL bWaitAll,_In_ DWORD dwMilliseconds);
  • nCount表示等待几个对象
  • lpHandles,等待的对象句柄数组
  • bWaitAll,表示是等待所有对象都变成触发状态再返回(TRUE),还是只要有一个对象触发就返回(FALSE)
  • dwMilliseconds 表示等待的时间
    如果bWaitAll为TRUE,返回值为WAIT_OBJECT_0表示全部触发
    如果bWaitAll为FALSE,返回值为WAIT_OBJECT_0表示lpHandles[0]对象触发,WAIT_OBJECT_0 + 1表示lpHandles[1]触发,以此类推。

再继续,我们设置的两次IO读取请求是从文件的不同偏移开始读的,我们来看下读取结果:

2.3 可提醒的I/O

可提醒IO是使用回调函数来实现,同时执行IO请求的函数有点变化,这里我们介绍RadFileEx和WriteFileEx,我们看下函数原型:

WINBASEAPI
_Must_inspect_result_
BOOL
WINAPI
ReadFileEx(_In_ HANDLE hFile,_Out_writes_bytes_opt_(nNumberOfBytesToRead) __out_data_source(FILE) LPVOID lpBuffer,_In_ DWORD nNumberOfBytesToRead,_Inout_ LPOVERLAPPED lpOverlapped,_In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);WINBASEAPI
BOOL
WINAPI
WriteFileEx(_In_ HANDLE hFile,_In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,_In_ DWORD nNumberOfBytesToWrite,_Inout_ LPOVERLAPPED lpOverlapped,_In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

和ReadFile及WriteFile,有几点不一样。

  • 这两个函数没有指向DWORD地址的指针表示已传输多少字节,毕竟在异步中不能立即拿到,该信息在回调函数才能得到
  • lpCompletionRoutine增加了这个参数,即回调函数的函数指针,看下类型:
typedef
VOID
(WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(_In_    DWORD dwErrorCode,_In_    DWORD dwNumberOfBytesTransfered,_Inout_ LPOVERLAPPED lpOverlapped);

错误码,传输的字节数,及LPOVERLAPPED 结构。
然后我们来看下例子,通过此来讲解下。

static bool readReady = false;
static BYTE bBuffer1[11] = { 0 };
static BYTE bBuffer2[11] = { 0 };VOID WINAPI ReadyFunction(ULONG_PTR param)
{static int times = 0;times++;if (times == 2) {readReady = true;}
}VOID WINAPI DoWorkRountine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED* lpOverlapped)
{if (lpOverlapped->Offset == 0) {std::cout << bBuffer1 << std::endl;}else {std::cout << bBuffer2 << std::endl;}QueueUserAPC(ReadyFunction, GetCurrentThread(), NULL);
}void DoWorkThd(void *param)
{HANDLE hFile = CreateFile(L"1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);if (hFile == INVALID_HANDLE_VALUE) {std::cout << "open error";return;}OVERLAPPED o1 = { 0 };o1.Offset = 0;ReadFileEx(hFile, bBuffer1, 10, &o1, DoWorkRountine);OVERLAPPED o2 = { 0 };o2.Offset = 10;ReadFileEx(hFile, bBuffer2, 10, &o2, DoWorkRountine);while (1) {if (readReady) {break;}SleepEx(500, TRUE);}
}int main()
{HANDLE tHandle = (HANDLE)_beginthread(DoWorkThd, 0, NULL);WaitForSingleObject(tHandle, INFINITE);return 0;
}

我们将DoWorkRountine作为IO完成的回调函数传入,其读出来的数据我们用两个全局变量来缓冲,我们注意到了发起IO请求的线程使用了SleepEx函数进去睡眠,我们看下这个函数:

WINBASEAPI
DWORD
WINAPI
SleepEx(_In_ DWORD dwMilliseconds,_In_ BOOL bAlertable);

和sleep相似,多了一个bAlertable参数,表示是否是可提醒的,如果是可提醒的,那么完成了IO请求完成后就会唤醒线程去执行回调函数。

  • 当系统创建一个线程,会创建一个与线程相关的待执行队列,这个队列被称为异步队列,在此当IO请求完成后,设备驱动程序就会在调用线程的异步队列中添加一项。当线程是可提醒的状态就会被激活去执行相关任务。且如果队列中至少有一项,那么系统就不会让线程进入到睡眠状态,当回调函数返回时,系统判断队列中是否有任务,如果有就会继续取出任务去执行,如果没有其他项,SleepEx等可提醒的函数返回,返回值是WAIT_IO_COMPLETION
  • Sleep函数内部也是调用了SleepEx,只是将bAlertable置为FALSE。其他可以将线程置为可提醒状态的还有WaitForSingleObjectEx,WaitForMultipleObjectEx,SingleObjectAndWaitEx,GetQueuedCompletionStatusEx,MsgWaitForMutipleObjectEx。

QueueUserAPC是允许我们手动往编程里添加任务。原型是:

WINBASEAPI
DWORD
WINAPI
QueueUserAPC(_In_ PAPCFUNC pfnAPC,_In_ HANDLE hThread,_In_ ULONG_PTR dwData);
  • pfnAPC是待执行的函数
  • hThread要添加的线程
  • dwData回调函数的自定义参数
    可提醒IO的确定很明显,回调函数没有足够地方存放上下文信息,需要一些全局变量,如我们例子中的bBuffer;第二个就是只能一个线程来完成IO请求和完成通知,不能用上多线程,可能对资源利用率不足。
    最后我们看下运行结果:

2.4 注意事项

由于篇幅限制,我们下一篇再讲述完成端口,剩下这里我们说下关于进行异步IO的时候注意事项

  • 当我们发起IO多个请求时,设备驱动程序并不会按照我们请求的顺序去执行(顺序是不一定的),所以大家尽量避免依靠顺序编码。
  • 当我们进行IO请求时,可能会同步返回,这是有可能系统之前有了这一部分的数据就会直接返回,所以大家需要在ReadFile等要判断返回值。
  • 我们在完成IO请求完成之前,一定要保证数据缓存和OVERLAPPED结构的存活,这些是在我们发起IO请求时只会传入地址,完成后会填充改地址的值。所以一定要保证他的存活性。

好了,就到这里了,参考自《windows核心编程》,欢迎交流

windows下异步IO一相关推荐

  1. Linux下异步IO(libaio)的使用以及性能

    Linux下异步IO是比较新的内核里面才有的,异步io的好处可以参考这里. 但是文章中aio_*系列的调用是glibc提供的,是glibc用线程+阻塞调用来模拟的,性能很差,千万千万不要用. 我们今天 ...

  2. windows下磁盘IO性能数据评测

    windows下如何查看磁盘IO性能 http://www.51testing.com/?uid-211722-action-viewspace-itemid-233892 服务器性能瓶颈如何判断.C ...

  3. linux下异步IO的简单例子

    首先,贴一下异步IO中用的的一些结构体,因为平常很少用,整理起来方便查看. aio.h中的struct aiocb struct aiocb{int aio_fildes; /* File desri ...

  4. windows下的IO模型之完成端口

    本文整理于:http://blog.csdn.net/piggyxp/article/details/6922277 一. 完成端口的优点 完成端口会充分利用Windows内核来进行I/O的调度,是用 ...

  5. Linux异步IO实现方案总结

    一.glibc aio 1.名称 由于是glibc提供的aio函数库,所以称为glibc aio. glibc是GNU发布的libc库,即c运行库. 另外网上还有其他叫法posix aio,都是指gl ...

  6. Windows下的网络编程Winsock

    文章目录 前言 1.服务器下的Winsock 1.1.构建编程环境: 1.2.WSAData结构体 1.3.WSAStartup初始化Winsock 1.4.WSACleanup释放Winsock 1 ...

  7. Windows内核原理-同步IO与异步IO

    目录 Windows内核原理-同步IO与异步IO 背景 目的 I/O 同步I/O 异步I/O I/O完成通知 总结 参考文档 Windows内核原理-同步IO与异步IO 背景 在前段时间检查异常连接导 ...

  8. windows 异步IO操作的几种实现方法

    文章目录 异步IO 设备内核对象 事件内核对象 可警醒IO 完成端口IO 异步IO 当我们读取一个文件时,一般情况下,线程是阻塞的,也就是说,当前线程在等待文件读取操作结束,这种方式叫同步IO. Wi ...

  9. 【免杀前置课——Windows编程】十四、异步IO——什么是异步IO、API定位问题、APC调用队列

    异步IO 异步IO 异步I/0注意事项: 定位问题 总解决方案 APC调用队列 异步IO 当我们读取一个文件时,一般情况下,线程是阻塞的,也就是说,当前线程在等待文件读取操作结束,这种方式叫同步IO. ...

最新文章

  1. 【知识发现】python开源哈夫曼编码库huffman
  2. Windows中的命令行提示符里的Start命令执行路径包含空格时的问题
  3. ERP选型 SAP PK Oracle
  4. 难道我的事,又要落空么。。。
  5. php轻量级的性能分析工具xhprof的安装使用
  6. 网络邻居无法共享解决办法
  7. django新建一个项目_如何使用Django创建项目
  8. 游戏UI设计(2.1)--窗口之父CXWnd的封装
  9. 【转】8G内存下MySQL的优化详细方案
  10. java代码审计工具_Java代码审计汇总系列(六)——RCE
  11. 解决网络和Internet设置中只有飞行模式的问题
  12. FaceBook到底验证个啥?
  13. 技术工作总结报告安全性设计报告用户手册如何编写
  14. potentially fixable with the `--fix` option.
  15. magicmatch java_签名图片一键批量生成 使用Java的Webmagic爬虫实现
  16. linux找出已经删除但磁盘空间未释放的大文件并清空
  17. 使用“for”循环遍历字典
  18. 初学者复现CornerNet:详细指导零基础在Ubuntu系统运行该代码并完全理解论文思路的教程
  19. 【高考那些事】准大学生看过来,选择方向和未来,自己把握
  20. magnify matlab 范围,matlabmagnify源程序(最新整理)

热门文章

  1. 深度学习——感知机(perceptron)图文详解
  2. Java——同步监视器
  3. Oracle数据库 触发器
  4. Element源码分析系列9-Switch(开关)
  5. MATLAB中libsvm工具箱的使用
  6. 基于深度学习的手写数字识别算法Python实现
  7. 云创地震监测预警系统荣膺“优秀产品奖”
  8. linux命令使用vim报错,LINUX中使用VIM替换报错:pattern not found
  9. 数学系花和计算机学霸爱情故事,理工学霸纷纷晒情书,看了数学系和化学系的表白,程序员表示不服...
  10. 用Netty实现单机百万TCP长连接