异步I/O基础
相比于计算机上的其他操作,I/O操作时最慢的最不可预测的操作之一。如果使用同步I/O,虽然方便控制,但是浪费了大量的CPU时间;而异步I/O在一定程度上缓解了这个问题。
异步I/O就是将I/O请求发送给设备驱动器,让设备驱动器负责实际的I/O操作,当设备驱动器在等待I/O设备相应时,应用程序的线程不用被挂起去等待I/O操作的完成,线程可以跳过等待继续执行其他任务。异步I/O的关键就是将所有的I/O请求队列化,然后以异步的方式执行I/O操作,在I/O操作完成之后再通知相应的程序。
如果编程使用异步I/O操作?首先,你在使用CreateFile函数打开或创建设备时,需要在dwFlagsAndAttributes参数指定FILE_FLAG_OVERLAPPED标志;然后在ReadFile或WriteFile函数的最后一个参数pOverlapped传入指定OVERLAPPED结构体。
OVERLAPPED结构体
结构体原型:
typedef struct _OVERLAPPED {
DWORD Internal; // [out] Error code
DWORD InternalHigh; // [out] Number of bytes transferred
DWORD Offset; // [in] Low 32-bit file offset
DWORD OffsetHigh; // [in] High 32-bit file offset
HANDLE hEvent; // [in] Event handle or data
} OVERLAPPED, *LPOVERLAPPED;
结构体的三个成员——Offset,OffsetHigh,hEvent——需要在调用CreateFile或ReadFile函数之前进行初始化的,而其他两个成员——Internal,InternalHigh——是执行I/O操作完成后设备驱动器负责设置的。
Offset &OffsetHigh:当一个文件被访问时,这两个成员组成一个64位的偏移量。请注意与同步I/O的不同,同步I/O时每个文件句柄都有一个与之对应的文件指针,标记每次文件读写的起始位置。而异步I/O由于其异步性,不能保证每次操作的顺序,如果不将每次读写的位置指定好,那么将会导致读写位置不确定,读写的内容也可能不是想到的。所以在异步I/O操作时,文件指针是直接被忽略的。
hEvent:事件句柄它记录了当I/O操作完成事通知的对象。具体的使用请看下面的”触发一个事件内核对象”。
Internal:这个成员标记了I/O操作处理过程中的错误码。当引发一个异步I/O请求时,设备将Internal置为STATUS_PENDING,表示没有错误出现(因为I/O操作还没开始)。事实上,WinBase.h中的宏HasOverlappedIoCompleted可以检测异步I/O操作是否完成,如果I/O请求没有完成,就返回FALSE.如果I/O操作完成则返回TRUE,宏定义如下:
#define HasOverlappedIoCompleted(pOverlapped) \
((pOverlapped)->Internal !=STATUS_PENDING)
InternalHigh:这个值,在I/O操作完成时,记录I/O操作的字节数。
异步I/O警告
对于异步I/O操作,有几点你需要注意:
第一:I/O设备驱动器对于队列化的I/O请求不一定要按照First-in First-out的顺序处理。
第二:很有必要对异步I/O用合适的方式进行错误检测。
第三:启用异步I/O请求的数据缓存区(data buffer)和OVERLAPPED结构体,不能在I/O请求完成之前被移动或销毁。因为当设备驱动器队列化I/O请求时,会记录数据缓冲区或OVERLAPPED结构体的地址(注意是地址,而不是内容,因为复制内容代价太高),所以如果你移动或销毁其内容,后果不堪设想。
上面的第三点非常重要,请看下面的代码:
VOID ReadData(HANDLE hFile) {
OVERLAPPED o = { 0 };
BYTE b[100];
ReadFile(hFile, b, 100, NULL, &o);
}
咋一看,你可能觉得上面的代码No problem。但是对照上面的第三点注意事项你就会发现不对,在ReadData函数体内,OVERLAPPED结构体或数据缓冲区b[100]都是分配在函数的stack里,当函数返回后这些数据都会被释放,而I/O操作时异步的,在函数退出后执行异步I/O操作时发现指针指向数据缓冲区和OVERLAPPED结构体的内容已经无效了,这显然就会出错。
取消队列化的I/O请求
有时,你可能需要在设备驱动器处理I/O请求之前取消某个已经队列化的I/O请求,Windows提供了几种取消的方式:
1) 调用CancelIo函数取消特定句柄相关的所有队列化的I/O请求.
BOOL CancelIo(HANDLE hFile)2) 你可以通过关闭设备本身来取消所有的I/O请求,不管哪个线程管理这些请求.
3) 当一个线程消亡时,系统会自动取消该线程的所有I/O请求(除了那些已经绑定到相应的I/O端口)。
4) 还可以调用CancelIoEx函数取消给定句柄发起的一个单独的特定的I/O请求。CancelIoEx函数原型:
BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped)。
值得注意的是:由于每次异步I/O都要指定一个OVERLAPPED结构体,所以每次调用CancelIoEx函数将取消一个I/O请求,但是如果pOverlapped为NULL,那个该函数就会取消hFile句柄相关的所有的异步I/O请求。