《Windows核心编程系列》谈谈????同步设备IO与异步设备IO之异步IO
同步设备IO与异步设备IO之异步IO介绍
设备IO与cpu速度甚至是内存访问相比较都是比较慢的,而且更不可预测。虽然如此,通过使用异步设备IO我们仍然能够创造出更高效的程序。
同步IO时,发出IO请求的线程会被挂起。而异步IO时发出请求的线程不会被挂起,而是可以继续执行。异步IO请求传给了设备驱动程序,被加入到驱动程序的请求队列中,驱动程序负责实际的IO操作。当设备驱动程序完成了对队列中IO请求的处理,无论成功与否都必须通知应用程序。
异步IO非常关键的一点就是将IO请求加入驱动程序队列,这是设计高性能可伸缩应用程序的本质所在。
为了以异步的方式来访问设备,首先在调用CreateFile时,必须传给dwFlagsAndAttributes参数FILE_FLAG_OVERLAPPED标志。这个标志告诉系统我们想要以异步的方式来访问设备。
其次,还需要将IO请求加入设备驱动程序的队列。我们需要ReadFile和WriteFile两个函数。这两个函数会检查hFile参数标识的设备是否FILE_FLAG_OVERLAPPED标志打开的。如果指定了这个标志,那么函数会执行异步设备IO。此时,可以传NULL给CreateFile和WriteFile的pdwNumBytes参数,因为它们会立即返回,检查pdwNumBytes是没有意义的。
再者,必须在pOverlapped参数总传入一个已初始化的OVERLAPPED结构。该结构定义如下:
typedef struct _OVERLAPPED{ DWORD Internal;//错误代码。 DWORD InternalHigh;//已传输字节数。 DWORD Offset;//低32位文件偏移。 DWORD OffsetHigh;//高32位文件偏移。 HANDLE hEvent;//事件对象句柄。}OVERLAPPED,*LPOVERLAPPED;
Offset,OffsetHigh,hEvent必须在调用ReadFile和WriteFile前初始化,Internal和InternalHigh有驱动程序设置。
Offset和OffsetHigh共同构成一个64位的偏移量。它表示在访问文件时应该从何处访问设备。
之所以会在此处提供偏移量是因为:在执行异步操作时系统会忽略文件指针。如果不再此处指定,那么系统将无法知道下次应该从哪里开始读取数据。为了防止在同一个对象上进行多个异步调用时出现混淆,所有的异步调用IO请求都必须在OVERLAPPED结构中指定起始偏移量。
非文件设备必须为这两个成员都指定为0,否则IO请求将会失败。GetLastError返回ERROR_INVALID_PARAMETER。
hEvent标识一个事件内核对象句柄。用来接收IO完成通知的4种方法(后面会有介绍)的最后一种:使用IO完成端口会使用到这个成员。
Internal成员用来保存已处理的IO请求的错误码。一旦我们发出一个异步IO请求,设备驱动程序会立即将Internal设为STATUS_PENDING,表示没有错误。通过检查此值我们可以使用HasOverlappedIoCompeleted宏检查异步IO请求是否已经完成。
该宏定义为:
#define HasOverlappedIoCompeleted(pOverlapped)\ ((pOverlapped)->Internal!=STATUS_PENDING)
InternalHigh用来保存已传输的字节数。
我们可以创建一个派生自OVERLAPPED结构的C++类,来扩展OVERLAPPED结构。如为派生类添加成员变量等。
异步设备IO注意事项:
一:设备驱动程序队列的异步设备IO请求不一定是以先入先出方式处理的。后被加入的请求也有可能先执行。
OVERLAPPED o1={0};OVERLAPPED o2={0};BYTE buff[1024];ReadFile(hFile,buff,100,NULL,&o1);WriteFile(hFile,buff,100,NULL,&o2);
二:我们以异步IO方式将IO请求添加到驱动程序队列中时,驱动程序会选择以同步的方式来处理请求。当我们从文件中读取数据时,系统会检查我们想要的数据是否在系统缓存中。如果数据已经在缓存中,系统就不会将我们的异步IO请求添加到设备驱动程序队列中。
如果请求的IO操作是以同步方式执行的,那么ReadFile和WriteFile会返回非零值。如果请求的IO操作是以异步方式执行的或者在调用函数时出现错误,函数会返回false。必须调用GetLastError来检查到底发生了什么事。如果GetLastError 返回ERROR_IO_PENDING,那么IO请求已经被就加入到了队列。其他值则说明IO请求没有被加入到驱动程序队列中。此时GetLastError会返回一下几个错误码:
ERROR_INVALID_USER_BUFFER或ERROR_NOT_ENOUGH_MEMORY.这两个错误码表示驱动程序请求队列已满,无法添加。
三:在异步IO请求完成之前,一定不能移动或是销毁发出IO请求所使用的数据缓存和OVERLAPPED结构。由于传给CreateFile和WriteFile的是OVERLAPPED结构的地址,当系统将IO请求加入设备驱动程序会将地址传给驱动程序。否则将会导致内存访问违规。
如:
VOID ReadData(HANDLE hFile){ OVERLAPPED o={0}; BYTE buff[100]; ReadFile(hFile,buff,100,NULL,&o);}
由于OVERLAPPED和buff是从栈中分配的,函数执行完毕栈被平衡,但异步IO请求可能还未执行完毕。此时再访问栈空间很容易导致访问违规。避免这种情况的方法可以从堆中分配内存。
有时候我们想要在设备驱动程序对一个已经加入加入队列的设备IO请求进行处理之前将其取消。
Windows为我们提供了多种方式:
一:调用CancelIo。该函数取消对该文件句柄的所有等待的I/O操作。
BOOL CancelIo(HANDLE hFile);
也可以关闭设备句柄,来取消所有已经添加到队列中的所有IO请求。
二:线程终止时,系统会自动取消该线程发出的所有IO请求。但如果请求的句柄具有与之相关联的IO完成端口,那么不在被取消之列。
三:CancelIoEx
BOOL CancelIoEx(HANDLE hFile,LPOVERLAPPED pOverlapped);
CancelIoEx能够取消给定文件句柄的一个指定IO请求。它会将hFile设备待处理的IO请求中所有与pOverlapped相关联的请求都标记为已经取消。由于每个待处理的IO请求都应该有自己的OVERLAPPED结构,因此每次调用CancelIoEx只取消一个待处理的请求。如果pOverlapped为NULL,那么CancelIoEx会将hFile指定的设备的所有待处理IO请求都取消掉。
被取消的IO请求会返回错误码ERROR_OPERATION_ABORTED。
接收IO请求完成通知。
现在我们已经知道如何将异步设备IO添加到驱动程序队列中,但是我们还没有介绍驱动程序如何通知我们IO请求已经完成。
Windows提供了4中方式来接收IO请求已经完成的通知。
一:触发设备内核对象。对向一个设备同时发出多个IO请求时,这种方法无用。
二:触发事件内核对象。
三:使用可提醒IO。
四:使用IO完成端口。
一:触发设备内核对象。
线程发出一个异步IO请求后,将继续执行。但即使如此,线程还需要在一点上等待设备内核对象被触发。
ReadFile和WriteFile函数在将IO请求添加到队列之前,会将设备内核对象设为非触发状态。当设备驱动程序完成了请求,驱动程序将设备内核对象设为触发状态。线程可以通过调用WaitForSingleObject或WaitForMultipleOBjecs来检查一个异步IO请求是否已经完成。
HANDLE hFile=CreateFile(...,FILE_FLAG_OVERLAPPED,...);BYTE buff[1000];OVERLAPPED o={0};o.Offset=345;BOOL bReadDone=ReadFile(hFile,bBuff,100,NULL,&o);//异步则返回false.DWORD ret=GetLastError();//异步则返回ERROR_IO_PENDIN。if(!bReadDone&&(ret==ERROR_IO_PENDING))//以异步IO执行。{ WaitForSingleObject(hFile,INFINITE); }
上述代码中首先发出了一个异步设备IO请求,然后立即等待请求完成。这就与同步设备IO请求无异,枉费了异步设备IO的设计意图。实际上并不怎么有用。如果向一个设备同时发出多个IO请求,这种方法是不行的。
二:触发事件内核对象
OVERLAPPED结构的最后一个成员hEvent来标识一个事件内核对象。我们需要调用CreateEvent来创建这个内核对象。当一个异步IO请求完成的时候,设备驱动程序会检查OVERLAPPED结构的hEvent成员是否为NULL。如果hEvent不为NULL,那么驱动程序会调用SetEvent来触发事件。为了检查异步IO是否完成可以等待事件内核对象被触发。
如果同时执行多个IO请求,就需要创建不同的事件对象,并初始化每个请求的OVERLAPPED结构中的hEvent成员,然后再调用ReadFile或WriteFile。
三:可提醒IO
虽然Windows花费数年开发出来的第三种用来接收IO完成通知的方法--可提醒IO。但是它非常的糟糕,但是辅助可提醒IO的基础设施还是非常有用的。应把主要精力放在这些基础设施上,不要纠缠与IO有关的方面。
当系统创建线程时会同时创建一个与线程相关联的队列。这个队列被称为异步过程调用队列(Asynchronous procedure call)。
当线程发出一个IO请求时,我们可以告诉设备驱动程序在调用线程的APC队列中添加一项。这需要调用一下两个函数:
BOOL ReadFileEx( HANDLE hFile, PVOID pvBuffer, DWORD nNumBytesToRead, OVERLAPPED*pOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);BOOL WriteFileEx( HANDLE hFile, PVOID pvBuffer, DWORD nNumBytesToRead, OVERLAPPED*pOverlapped, LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);
它们在将IO请求发给设备驱动程序后会立即返回。*Ex要求我们传入一个函数地址,这个回调函数被称为完成函数。
VOID WINAPI CompletionRoutine( DWORD dwError, DWORD dwNumBytes, OVERLAPPED*po);
当我们用ReadFileEx和WriteFileEx发出一个IO请求时,这两个函数会将回调函数地址传给设备驱动程序。当设备驱动程序完成IO请求时,会在发出IO请求的线程的APC队列中添加一项。该项包括完成函数地址,以及OVERLAPPED结构地址。
回调函数并不会立即被调用。为了对线程APC队列中的项进行处理,线程必须将自己置为可提醒状态。Windows提供了六个函数可以将线程置为可提醒状态。
DWORD SleepEx( DWORD dwMilliSeconds, BOOL bAlertable);DWORD WaitForSingleObjectEx( HANDLE hObject, CONST HANDLE*phObjects, BOOL bWaitAll, BOOL dwMilliseconds, BOOL bAlertable);BOOL GetQueuedCompletionStatusEx( HANDLE hCompPort, LPOVERLAPPED_ENTRY pCompPortEntries, DWORD dwMilliSeconds, BOOL aAlertable);BOOL GetQueuedCompletionStatusEx( HANDLE hCompPort, LPOVERlAPPEDENTRY pCompPortEntries, ULONG ulCount, PULONG pulNumNumEntriesRemoved, BOOL bAlertable);DWORD MsgWaitForMultipleObjectEx( DWORD nCount, CONST HANDLE *pHandles, DWORD dwMilliseconds, DWORD dwWakeMask, DWORD dwFlags);
前五个函数的最后一个参数是布尔变量,用来表示调用线程是否应该将自己置为可提醒状态。对最后一个函数MsgWaitForMultipleObjectEx来说,需要使用MWMO_ALERTABLE标志来让线程进入可提醒状态。
当线程由于调用上述六个函数即将挂起并处于可提醒状态时,系统会检查它的APC队列,对队列中的每一项系统会调用完成函数,并传给该函数IO错误码(OVERLAPPED结构的Internal成员),已传输字节数
(OVERLAPPED的InternalHigh成员)以及OVERLAPPED结构地址。
当线程调用上述任何一个函数等待一个内核对象被触发并将线程置为可提醒状态时,系统会首先检查线程的APC队列。如果APC队列有项,那么系统不会让线程进入睡眠状态。系统会将APC队列中的项取出,让线程调用回调函数,并传入相应参数。直到所有的项都被处理,此时等待函数(上面介绍的6个函数)才会让线程挂起。当APC队列中没有项时,线程将会被挂起直到等待传入的内核对象被触发。一旦APC出现新项,线程会立即被唤醒。因为线程处于可提醒状态,可以随时被唤醒。
综上我们可以知道,导致线程被唤醒有三种情况:
1:等待的内核对象被触发。
2:APC队列出现一项。
3:超出了等待时间。
可提醒IO的优劣:
可提醒IO要求我们必须创建一个回调函数,这使得代码变得很复杂。我们必须在全局空间中加入大量信息。
发出IO请求的线程,必须同时对完成通知进行处理。这使得程序的伸缩性不太好。
基于以上两个问题,作者不推荐使用可提醒IO来接收IO完成通知。
Windows提供一个函数允许手动的添加项到APC队列中
DWORD QueueUserAPC( PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData);
pfnAPC是一个指向APC函数指针。它必须符合一下原型:
VOID WINAPI APCFUNC(ULONG_PTR dwParam);
hThread是线程句柄,用来告诉系统想要将项添加到那个线程队列
dwData是传给回调函数的参数。
我们可以使用QueueUserAPC来进行非常高效的线程间通信,甚至能够跨进程通信。我们可以手动的向另一线程的APC队列中添加APC 调用项,并传入参数,实现线程间通信。
QueueUserAPC还可以强制线程退出等待状态。当线程一由于调用WaitForSingleObjectEx,并将其置为可提醒状态时。主线程可以调用QueueUserAPC将一个APC项添加到线程一的队列中。此时线程就被唤醒,并调用APCFunc函数。当APC队列中没有项被处理时WaitForSingleObjectEx函数返回。返回值为WAIT_IO_COMPLETIO。线程一需要检查这个返回值才知道自己应该退出。
VOID WINAPI APCFunc(ULONG_PTR dwParam){}UINT WINAPI ThreadFunc(PVOID pvParam){ HANDLE hEvent=(HANDLE)pvParam; DWORD dw=WaitForSingleObjectEx(hEvent,INFINITE,true); if(dw==WAIT_OBJEC_0)//事件内核对象被触发。 { } if(dw==WAIT_IO_COMPLETION)//线程退出。 { return 0; }}
四:IO完成端口
为了将Windows打造成一个出色的服务器环境,Microsoft开发出了IO完成端口。完成端口需要与线程池配合使用。
完成端口背后的理论是并发运行的线程数量必须有一个上限。由于太多的线程将会导致系统花费很大的代价在各个线程cpu上下文进行切换。
使用并发模型与创建进程相比开销要低很多,但是也需要为每个客户请求创建一个新的线程。这开销仍然很大。通过使用线程池可以是性能有很大的提高。IO完成端口需要配合线程池配合使用。
IO完成端口也是一个内核对象。调用以下函数创建IO完成端口内核对象。
HANDLE CreateIoCompletionPort( HANDLE hFile, HANDLE hExistingCompletionPort, ULONG_PTR CompletionKey, DWORD dwNumberOfConcurrentThreads);
这个函数会完成两个任务:
一是创建一个IO完成端口对象。
二是将一个设备与一个IO完成端口关联起来。
hFile就是设备句柄。
hExistingCompletionPort是与设备关联的IO完成端口句柄。为NULL时,系统会创建新的完成端口。
dwCompletionKey是一个对我们有意义的值,但是操作系统并不关心我们传入的值。一般用它来区分各个设备。
dwNumberOfConcurrentThreads告诉IO完成端口在同一时间最多能有多少进程处于可运行状态。如果传入0,那么将使用默认值(并发的线程数量等于cpu数量)。在第二章我们曾介绍说几乎所有的内核对象都需要安全属性参数。那时的几乎就是因为IO完成端口这个例外。它是唯一一个不需要安全属性的内核对象。 这是因为IO完成端口在设计时就是只在一个进程中使用。
每次调用CreateIoCompletionPort时,函数会判断hExistingCompletionKey是否为NULL,如果为NULL,会创建新的完成端口内核对象。并为此完成端口创建设备列表然后将设备加入到此完成端口设备列表中(先入先出)。
设备列表存储与该完成端口相关联的所有设备。
设备列表只是调用CreateIoCompletionPort函数时的一个数据结构。除此之外还有四个结构。
第二个结构是IO完成队列。当设备的一个异步IO请求完成时,系统会检查该设备是否与一个完成端口相关联,如果关联,系统会将这个已完成的IO请求添加到完成端口的IO完成队列中。每一项包括已传输字节数,完成键(dwCompletionKey)值,以及一个指向IO请求的OVERLAPPED结构指针和错误码。
Windows为IO完成端口提供了一个函数,可以将线程切换到睡眠状态,来等待设备IO请求完成并进入完成端口。
BOOL GetQueuedCompletionStatus( HANDLE hCompletionPort, PDWORD pdwNumberOfBytesTransferred, ULONG_PTR pCompletionKey, OVERLAPPED** ppOverlapped, DWORD dwMilliSeconds);
hCompletionPort表示线程希望对哪个完成端口进行监视,GetQueuedCompletionStatus的任务就是将调用线程切换到睡眠状态,也就是阻塞在此函数上,直到指定的IO完成端口出现一项或者超时。
pdwNumberOfBytesTransferred返回在异步IO完成时传输的字节数。
pCompletionKey返回完成键。
ppOverlapped返回异步IO开始时传入的OVERLAPPED结构地址。
dwMillisecond指定等待时间。
函数执行成功则返回true,否则返回false。
第三个结构是等待线程队列。当线程池中的每个线程调用GetQueuedCompletionStatus时,调用线程的线程标识符会被添加到这个等待线程队列,这使得IO完成端口对象能知道,有哪些线程当前正在等待对已完成的IO请求进行处理。当IO完成端口的IO完成队列中出现一项时,完成端口会唤醒等待线程队列中的一个线程。这个线程会得到已完成IO项的所有信息:已传输字节数,完成键以及OVERLAPPED结构地址。这些信息是通过传给GetQueuedCompletionStatus的参数来返回的。
IO完成队列中的各项是以先入先出方式来进行的。但是唤醒等待队列中的线程是按照后入先出的方式进行。假设有四个线程正在等待队列中等待,如果出现了一个已完成的IO项,那么最后一个由于调用GetQueuedCompletionStatus而被挂起的线程会被唤醒来处理这一项。当处理完该项后,线程会由于再次调用GetQueuedCompletionStatus而进入等待线程队列。使用这种算法,系统可以将哪些长时间睡眠的线程换出到磁盘。
作者一直在推崇IO完成端口。接下来我们来讨论下为什么IO完成端口这么有用!!!
前面我们提到过IO完成端口只有配合线程池才能发挥更大的作用。当我们创建并关联设备时,需要指定有多少个线程并发运行。一般将这个值设置为cpu的数量。当已完成的IO项被添加到完成队列中时,IO完成端口会唤醒正在等待的线程,但是唤醒的线程数最多不会超过我们指定的数量。如果有四个IO请求已完成,且有四个线程等待GetQueuedCompletionStatus而被挂起,那么IO完成端口只唤醒两个线程处理,另外两个线程继续睡眠。此时读者可能会疑问:既然完成端口只允许唤醒指定数量的线程,那么为什么还指定更多的线程在线程池中呢?这就涉及到IO完成端口的第四个数据结构:已释放线程列表。它存储已被唤醒的线程句柄。这使得IO完成端口能够直到哪些线程已经被唤醒并监视它们的执行情况。如果此时已释放线程由于调用某些函数将线程切换到了等待状态,完成端口会将其从已释放队列中移除,并将其添加到已暂停线程列表。
已暂停队列是IO完成端口第五个数据结构。
完成端口的目标是根据创建完成端口时指定的并发线程数量,将尽可能多的线程保持在已释放线程列表中。如果一个已暂停的线程被唤醒,它会离开已暂停线程列表并重新进入已释放线程列表。这意味着已释放列表中的线程数量将大于最大允许的并发线程数量。这句话什么意思呢?正在运行的线程数量加上从暂停线程列表中被释放的线程数量使总数大于最大允许的数量。这可以使线程数量在短时间内超过指定数量。
IO完成端口并不一定要用于设备IO,它还可以进行线程间通信。在可提醒IO中我们介绍了QueueUserAPC。该函数允许线程将一个APC项添加到另一个线程的队列中。IO完成端口也存在一个类似的函数:
PostQueuedCompletionStatus;
BOOL PostQueuedCompletionStatus( HANDLE hCompletionPort, DWORD dwNumBytes, ULONG_PTR CompletionKey, OVERLAPPED*pOverlapped);
这个函数用来将已完成的IO通知追加到IO完成端口的队列中。
hCompletionPort表示我们要将已完成的IO项添加到哪个完成端口的队列中。
剩下的三个参数表示应该返回给主调线程的值。
PostQueuedCompletionStatus函数非常有用。它为我们提供了一种方式与线程池中所有线程通信。例如:当用户终止服务程序时,我们想要所有线程退出。如果各个线程还在等待完成端口但有没有已完成的IO请求,那么无法将它们唤醒。我们可以为线程池中的每个线程都调用一次GetQueuedCompletionStatus,将它们都唤醒。各线程对GetQueuedCompletionStatus函数返回值进行检查,如果发现应用程序正在终止,就会正常退出。(由于线程等待队列是以栈方式唤醒各线程,为了保证线程池中每个线程都有机会得到模拟IO项,我们还必须在程序中采用其他线程同步机制)
当对完成端口调用CloseHandle时,系统会将所有正在等待GetQueuedCompletionStatus返回的线程唤醒,并返回false。GetLastError返回ERROR_INVALID_HANDLE,此时线程就可以知道应该退出了。
下面的例子使用异步IO 实现了文件复制工作。首先选择要复制的文件,并取得源文件大小。点击复制按钮创建一个线程。新线程将完成创建IO完成端口和关联设备及执行文件复制工作。将源文件和目标文件都关联到同一个完成端口,根据GetQueuedCompletionStatus返回时的完成键来区分到底是属于谁的异步IO返回。在启动循环时采用了一个小伎俩:使用PostQueuedCompletionStatus向IO完成端口发送一个模拟的异步IO请求。完成键设置为WRITE_KEY。此时程序将执行从源文件读数据操作。这样就开动了引擎。直到文件复制完成。注意源文件和目标文件以及GetQueuedCompletionStatus使用的OVERLAPPED结构不要使用同一个。 选择文件函数:
void CIOCompletionPortDlg::OnBnClickedBtnChoosefile(){// TODO: 在此添加控件通知处理程序代码CFileDialog dlg(true);dlg.DoModal();m_fileName=dlg.GetPathName();SetDlgItemText(IDC_EDIT_FILENAME,m_fileName);m_hSrcFile=CreateFile(m_fileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_FLAG_OVERLAPPED,NULL);if(m_hSrcFile==INVALID_HANDLE_VALUE){return ;}DWORD filesize;DWORD filesizeHigh;m_SrcFileSize=GetFileSize(m_hSrcFile,&filesizeHigh);DWORD t=m_SrcFileSize/1024.0;//filesize/=1024.0;CString temp;temp.Format(TEXT("%d KB"),t);SetDlgItemText(IDC_EDIT_FILESIZE,temp);}//创建任务线程:void CIOCompletionPortDlg::OnBnClickedBtnCopy(){// TODO: 在此添加控件通知处理程序代码m_hCopyThread=CreateThread(NULL,0,CopyThread,this,0,NULL);//CloseHandle(m_hCopyThread);}//新线程入口函数:
DWORD WINAPI CIOCompletionPortDlg::CopyThread( PVOID ppram ){CIOCompletionPortDlg *pdlg=(CIOCompletionPortDlg*)ppram;pdlg->m_hDesFile=CreateFile(TEXT("备份.exe"),GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_FLAG_OVERLAPPED,pdlg->m_hSrcFile);LARGE_INTEGER filesize;filesize.HighPart=0;filesize.LowPart=pdlg->m_SrcFileSize;SetFilePointerEx(pdlg->m_hDesFile,filesize,NULL,FILE_BEGIN);SetEndOfFile(pdlg->m_hDesFile);//创建IO完成端口。创建一个完成端口,将两个设备将关联到此完成端口上。HANDLE hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,4);//创建完成端口,但不关联设备。if(hIOCP==NULL){return 0;}CreateIoCompletionPort(pdlg->m_hSrcFile,hIOCP,READ_KEY,0);//与IO完成端口关联。CreateIoCompletionPort(pdlg->m_hDesFile,hIOCP,WRITE_KEY,0);//在关联时传入了完成键。可以根据完成键来区别从完成队列中取出的请求属于哪个设备。OVERLAPPED ov={0};PostQueuedCompletionStatus(hIOCP,0,WRITE_KEY,&ov);//发送模拟完成异步IO消息。BYTE *pBuffer=new BYTE[BUFFERSIZE];OVERLAPPED ovDes={0};OVERLAPPED ovSrc={0};while(true){//memset(pBuffer,0,sizeof(pBuffer));DWORD nTransfer;OVERLAPPED *overlapped;ULONG_PTR CompletionKey;GetQueuedCompletionStatus(hIOCP,&nTransfer,&CompletionKey,(OVERLAPPED**)&overlapped,INFINITE);//IO完成队列没有请求项则挂起。否则从IO完成队列取出。switch(CompletionKey){case READ_KEY://从IO完成端口取出读完成。{ BOOL r=WriteFile(pdlg->m_hDesFile,pBuffer,overlapped->InternalHigh,NULL,&ovDes);ovDes.Offset+=BUFFERSIZE;}break;case WRITE_KEY://从IO完成队列中取出写完成。{memset(pBuffer,0,BUFFERSIZE);if(ovSrc.Offset<pdlg->m_SrcFileSize){DWORD nBytes;if(ovSrc.Offset+BUFFERSIZE<pdlg->m_SrcFileSize)nBytes=BUFFERSIZE;elsenBytes=pdlg->m_SrcFileSize-ovSrc.Offset;ReadFile(pdlg->m_hSrcFile,pBuffer,nBytes,NULL,&ovSrc);//异步IO忽略文件指针。所有对文件的定位操作由OVERLAPPED结构指定。//一定要注意为每次异步IO请求提供一个OVERLAPPED结构。刚才由于在接收和发送使用了//同一个OVERLAPPED结构,导致出现重叠 I/O 操作在进行中。错误代码:997ovSrc.Offset+=BUFFERSIZE;//OVERLAPPED的OffsetHigh结构必须每次都得设置。}else{::MessageBox(NULL,TEXT("文件复制完成"),TEXT(""),MB_OK);return 0;}}break;default:break;}}return 0;}运行结果:



本文参考自《Windows核心编程》第五版第二部分。如有错误,请不吝赐教!!! 2012.12.18 20:52于山西大同- 1楼ithzhang昨天 11:58
- 源码奉上!!!