异步设备IO OVERLAPPED结构(设备内核对象 事件内核对象 可提醒IO)
同步IO是指:线程在发起IO请求后会被挂起,IO完成后继续执行。
异步IO是指:线程发起IO请求后并不会挂起而是继续执行。IO完毕后会得到设备驱动程序的通知。
一.异步准备与OVERLAPPED结构
(1).为了以异步的方式来访问设备,必须先调用CreateFile,并在dwFlagsAndAttributes参数中指定FILE_FLAG_OVERLAPPED标志来打开设备。该标志告诉系统要以异步的方式来访问设备。
为了将I/O请求加入设备驱动程序的队列中,必须使用ReadFile和WriteFile函数:
1 2 3 4 5 6 7 8 9 | HANDLE CreateFile( LPCTSTR lpFileName, // 文件名/设备路径 设备的名称 DWORD dwDesiredAccess, // 访问方式 DWORD dwShareMode, // 共享方式 LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全描述符指针 DWORD dwCreationDisposition, // 创建方式 DWORD dwFlagsAndAttributes, // 文件属性及标志 HANDLE hTemplateFile // 模板文件的句柄 ); |
当调用ReadFile,WriteFile这两个函数中任何一个时,函数会检查hFile参数标识的设备是否用FILE_FLAG_OVERLAPPED标志打开的。如果打开设备时指定了这个标志,那么函数会执行异步设备I/O。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | BOOL WINAPI ReadFile( _In_ HANDLE hFile, _Out_ LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Out_opt_ LPDWORD lpNumberOfBytesRead, _Inout_opt_ LPOVERLAPPED lpOverlapped ); BOOL WINAPI ReadFile( _In_ HANDLE hFile, _Out_ LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Out_opt_ LPDWORD lpNumberOfBytesRead, _Inout_opt_ LPOVERLAPPED lpOverlapped ); |
(2).再来看OVERLAPPED结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 | typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; union { struct { DWORD Offset; DWORD OffsetHigh; } DUMMYSTRUCTNAME; PVOID Pointer; } DUMMYUNIONNAME; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED; |
1.Internal成员:这个成员用来保存已处理的I/O请求的错误码.
InternalHigh成员:当异步I/O请求完成的时候,这个成员用来保存已传输的字节数。
2..Offset和OffsetHigh成员,构成一个64位的偏移量,它们表示当访问文件的时候应该从哪里开始进行I/O操作。每个文件内核对象都有一个与之相关联的文件指针。在执行异步I/O的时候,系统会忽略文件指针。这是为了避免在对同一个对象进行多个异步调用的时候出现混淆,所有异步I/O请求必须在OVERLAPPED结构中指定起始偏移量。非文件设备会忽略这两个参数,必须将其初始化为0,否则I/O请求会失败。
(3.)异步设备IO注意事项
1:异步IO不会按照你的投递顺序来执行,驱动会选择他认为最快的方式来组合这些投递
2:错误处理,以文件IO为例,当我们投递一个异步ReadFile()时,设备驱动程序可能会以同步方式执行,例如如果设备驱动程序发现要读取的数据在文件缓冲里时,就不会投递这个异步设备IO,而是直接将数据复制进我们的缓冲区
3.如果IO是同步方式执行,ReadFile()和WriteFile()返回非零值,如果是异步或者出现错误,返回FALSE,调用GetLastError()获得错误码,如果返回的是ERROR_IO_PENDING,那么IO请求已经被成功地加入了队列。
二.接收IO请求完成的方法
Windows提供了4种不同的技术方法来得到I/O完成的通知。
技术 | 概要 |
通知一个设备内核对象 | 当一个设备同时有多个IO请求的时候,该方法不适用。允许一个线程发送一个IO请求,另一个线程处理之。 |
通知一个事件内核对象 | 允许一个设备同时有多个IO请求。允许一个线程发送一个IO请求,另一线程处理之。 |
警告IO | 允许一个设备同时有多个IO请求。必须在同一个线程中发送并处理同一个IO请求。 |
IO完成端口 | 允许一个设备同时有多个IO请求。允许一个线程发送一个IO请求,另一个线程处理之。该方法伸缩性好,而且性能高。 |
1.触发设备内核对象
(1)Read/WriteFile在将I/O请求添加到队列之前,会先将对象设为未触发状态。当设备驱动程序完成了请求之后,会将设备内核对象设为触发状态,线程可以通过WaitForSingalObject或WaitForMultiObject来检查一个异步IO请求是否完成。
(2)不能向同一个设备发出多个IO请求。
2.触发事件内核对象
(1)在每个I/O请求的OVERLAPPED结构体的hEvent创建一个用来监听该请求完成的事件对象。当一个异步I/O请求完成时,设备驱动程序会调用SetEvent来触发事件。驱动程序仍然会像从前一样,将设备对象也设为触发状态,因为己经有了可用的事件对象,所以可以通过SetFileCompletionNoticationModes(hFile,FILE_SKIP_SET_EVENT_ON_HANDLE)来告诉操作系统在操作完成时,不要触发文件对象。
3.使用可提醒IO
(1)创建线程时,会同时创建一个与线程相关联的APC队列(异步过程调用),可以告诉设备程序驱动程序在I/O完成时,为了将通知信息添加到线程的APC队列中,需要调用ReadFileEx和WriteFileEx函数。
(2)ReadFile/WriteFileEx函数与Read/WriteFile最大的不同在于最后一个参数,这是一个回调函数(也叫完成函数)的地址,当*Ex发出一个I/O请求时,这两个函数会将回调函数的地址传给设备驱动程序。当设备驱动程序完成I/O请求的时候,会在发出I/O请求的线程的APC队列中添加一项。该项包含了完成函数的地址以及发出I/O请求时使用的OVERLAPPED的地址。
对于这种异步设备I/O方式,确实没什么用。重要的是微软为可提醒IO构建的基础设施——APC(Asynchronous Procedure Call),异步过程调用。
当创建一个线程的时候,系统会为线程维护一个APC队列,该队列中的项目想要得到执行,线程必须处于可提醒等待状态,即使用SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函数,并且把最后一个参数设置为TRUE。当线程处于可提醒等待状态时,线程就会执行APC队列中的APC,之后执行过的APC就会清除队列,再进行下一次执行APC(如果APC队列中还有未执行的APC)。
在这一小节中,作者的用意就是不要使用可提醒I/O方式进行异步设备I/O——因为可提醒I/O的两大缺陷:回调函数的累赘和线程的无负载均衡机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | // Overlapped.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <windows.h> #include <iostream> /* Internal成员:这个成员用来保存已处理的I/O请求的错误码. InternalHigh成员:当异步I/O请求完成的时候,这个成员用来保存已传输的字节数。 在当初设计OVERLAPPED结构的时候,Microsoft决定不公开Internal和InternalHigh成员(名副其实)。随着时间的推移,Microsoft认识到这些成员包含的信息会对开发人员有用,因此把它们公开了。但是,Microsoft没有改变这些成员的名字,这是因为操作系统的源代码频繁地用到它们,而Microsoft并不想为此修改源代码。 */ using namespace std; #define PAGE_SIZE 0x1000 void Sub_1(); //ReadFile 异步操作 void Sub_2(); //ReadFileEx DWORD WINAPI Sub_1ThreadProcedure( LPVOID ParameterData); DWORD WINAPI Sub_2ThreadProcedure( LPVOID ParameterData); OVERLAPPED __Overlapped = { 0 }; char __BufferData[4] = {0}; int main() { //Sub_1(); //触发事件内核对象 Sub_2(); //可提醒IO } void Sub_1() { BOOL IsOk = FALSE; DWORD ReturnLength = 0; HANDLE FileHandle = CreateFile(L "ReadMe.txt" , GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); if (FileHandle == INVALID_HANDLE_VALUE) { int LastError = GetLastError(); goto Exit; } //当一个异步IO请求完成的时候,驱动程序检查OVERLAPPED结构的hEvent成员是否为NULL //如果hEvent不为NULL,那么驱动程序会调用SetEvent来触发事件,这时候就是使用事件对象来检查一个设备操作是否完成,而不是等待设备(文件)对象 __Overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //绝对要创建 HANDLE ThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Sub_1ThreadProcedure, ( LPVOID )FileHandle, 0, NULL); if (__BufferData == NULL) { goto Exit; } IsOk = ReadFile(FileHandle, __BufferData, 4, &ReturnLength, &__Overlapped); //事件必须创建 if (IsOk == FALSE) { int LastError = GetLastError(); if (LastError == ERROR_IO_PENDING) { //成功 printf ( "ERROR_IO_PENDING\r\n" ); //重叠I/O返回标志 } } WaitForSingleObject(ThreadHandle, INFINITE); Exit: if (FileHandle != NULL) { CloseHandle(FileHandle); FileHandle = NULL; } printf ( "\r\n" ); return ; } DWORD WINAPI Sub_1ThreadProcedure( LPVOID ParameterData) { HANDLE FileHandle = ( HANDLE )ParameterData; BOOL IsOk = FALSE; DWORD ReturnLength = 0; while (1) { IsOk = WaitForSingleObject(__Overlapped.hEvent, INFINITE); IsOk -= WAIT_OBJECT_0; if (IsOk == 0) { IsOk = GetOverlappedResult(FileHandle, &__Overlapped, &ReturnLength, INFINITE); if (IsOk==TRUE) { int i = 0; for (i = 0; i < ReturnLength; i++) { printf ( "%c" , __BufferData[i]); } __Overlapped.Offset += ReturnLength; ReadFile(FileHandle, &__BufferData, 4, &ReturnLength, &__Overlapped); } else { //数据完毕 break ; } } else { return 0; } } return 0; } void Sub_2() { BOOL IsOk = FALSE; HANDLE FileHandle = CreateFile(L "ReadMe.txt" , GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); if (FileHandle == INVALID_HANDLE_VALUE) { int LastError = GetLastError(); goto Exit; } //__Overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //不能提供该事件 HANDLE ThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Sub_2ThreadProcedure, ( LPVOID )FileHandle, 0, NULL); if (__BufferData == NULL) { goto Exit; } IsOk = ReadFileEx(FileHandle, __BufferData, 4, &__Overlapped, NULL); if (IsOk == FALSE) { int LastError = GetLastError(); if (LastError == ERROR_IO_PENDING) { //成功 printf ( "ERROR_IO_PENDING\r\n" ); //重叠I/O返回标志 } } WaitForSingleObject(ThreadHandle, INFINITE); Exit: if (FileHandle != NULL) { CloseHandle(FileHandle); FileHandle = NULL; } printf ( "\r\n" ); return ; } DWORD WINAPI Sub_2ThreadProcedure( LPVOID ParameterData) { HANDLE FileHandle = ( HANDLE )ParameterData; DWORD ReturnLength = 0; BOOL IsOk = FALSE; while (1) { IsOk = GetOverlappedResult(FileHandle, &__Overlapped, &ReturnLength, TRUE); //当一个可提醒IO完成时,设备驱动程序不会试图去触发一个事件对象 //IsOk = WaitForSingleObject(__Overlapped.hEvent, INFINITE); if (IsOk == TRUE) { int i = 0; for (i = 0; i < ReturnLength; i++) { printf ( "%c" , __BufferData[i]); } __Overlapped.Offset += ReturnLength; ReadFileEx(FileHandle, &__BufferData, 4, &__Overlapped, NULL); } else { return 0; } } return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | // Overlapped.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <windows.h> #include <iostream> using namespace std; VOID CALLBACK CompletionRoutine( _In_ DWORD ErrorCode, _In_ DWORD ReturnLength, _Inout_ LPOVERLAPPED Overlapped); HANDLE __FileHandle = NULL; char __BufferData[20] = {0}; int main() { BOOL IsOk = FALSE; OVERLAPPED Overlapped = { 0 }; __FileHandle = CreateFile(L "ReadMe.txt" , GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, NULL); if (__FileHandle == INVALID_HANDLE_VALUE) { int LastError = GetLastError(); goto Exit; } IsOk = ReadFileEx(__FileHandle, __BufferData, 4,&Overlapped, (LPOVERLAPPED_COMPLETION_ROUTINE)CompletionRoutine); if (IsOk == FALSE) { int LastError = GetLastError(); if (LastError == ERROR_IO_PENDING) { //成功 } } Exit: SleepEx(0,TRUE); if (__FileHandle != NULL) { CloseHandle(__FileHandle); __FileHandle = NULL; } printf ( "Input AnyKey To Exit\r\n" ); getchar (); return 0; } VOID CALLBACK CompletionRoutine( _In_ DWORD ErrorCode, _In_ DWORD ReturnLength, _Inout_ LPOVERLAPPED Overlapped ) { if (ErrorCode == ERROR_SUCCESS) { int i = 0; for (i = 0; i < ReturnLength; i++) { printf ( "%c" , __BufferData[i]); } Overlapped->Offset += ReturnLength; ReadFileEx(__FileHandle, __BufferData, 4, Overlapped, (LPOVERLAPPED_COMPLETION_ROUTINE)CompletionRoutine); } else if (ErrorCode==ERROR_HANDLE_EOF) { //数据完成 printf ( "\r\n" ); } else { } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗