10.5 接收I/O请求完成的通知
(1)I/O请求被加入设备驱动程序的队列,当请求完成以后,设备驱动也要负责通知我们I/O请求己经完成。
(2)可以用4种方法来接收I/O请求己经完成的通知
技术 |
特点 |
触发设备内核对象 |
①允许一个线程发出I/O请求,另一个线程对结果进行处理。 ②当向一个设备同时发出多个I/O请求的时候,这种方法是不能用的,因为等待函数中等待的是同一个内核对象,只要任何一个I/O请求完成时都会被触发,却没办法区别是哪个请求的完成触发了内核对象。 |
触发事件内核对象 |
①允许一个线程发出I/O请求,另一个线程对结果进行处理。 ②允许我们向一个设备同时发出多个I/O请求的时候。(因为每个请求都通过pOverlapped与一个事件相关联) |
使用可警告I/O |
①发出I/O请求的线程必须对结果进行处理,因为这是通过线程的APC队列来实现的,而APC队列是线程独有的。 ②允许我们向一个设备同时发出多个I/O请求的时候。 |
使用I/O完成端口 |
①允许一个线程发出I/O请求,另一个线程对结果进行处理。 ②允许我们向一个设备同时发出多个I/O请求的时候。 ③这项技术具有高度的伸缩性和最佳的灵活性 |
10.5.1 通过触发设备内核对象来通知I/O处理己完成
(1)Read/WriteFile在将I/O请求添加到队列之前,会先将对象设为未触发状态。当设备驱动程序完成了请求之后,会将设备内核对象设为触发状态。
(2)使用这种方法不能达到异步调用的好处,因为发出请求以后,要立即等待请求的完成,这跟同步调用效果是一样的。
【示例代码】——在实际的代码中,不用这种方式来获取通知,因为没能真正体现异步的好处,也不能处理多个I/O请求。
//以异步方问访问hFile设备(传入参数FILE_FLAG_OVERLAPPED) HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...); BYTE bBuffer[100]; OVERLAPPED o = { 0 }; o.Offset = 345; //从第346个字节开始读取数据 BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o); //添加异步I/O请求 DWORD dwError = GetLastError(); //ReadFile异步调用会立即,返回值为FALSE,GetLastError为ERROR_IO_PENDING表示 //请求正在被处理 if (!bReadDone && (dwError == ERROR_IO_PENDING)){ //I/O请求正在被异步处理,等待I/O请求完成的通知 WaitForSingleObject(hFile, INFINITE); //等待的是设备内核对象! bReadDone = TRUE; } if (bReadDone){ //读取完成后,数据被写入bBuffer,状态信息写入pOverlapped结构体中 //o.Internal包含IO错误代码 //o.InternalHigh包含己经传输的字节数 //bBuffer包含读取到的数据 } else{ //出现错误,可查看dwError以获得更多信息 }
【示例程序2】——用来说明设备内核对象不能处理多个IO请求
//以异步方问访问hFile设备(传入参数FILE_FLAG_OVERLAPPED) HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...); //异步I/O读请求 BYTE bReadBuffer[10]; OVERLAPPED oRead = { 0 }; oRead.Offset = 0; ReadFile(hFile, bReadBuffer, 100, NULL, &o); //添加异步I/O读请求 //异步I/O写请求 BYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; OVERLAPPED oWrite = { 0 }; oWrite.Offset = 10; WriteFile(hFile, bWriteBuffer,_countof(bWriteBuffer), NULL, &oWrite); ... WaitForSingleObject(hFile, INFINITE); //hFile被触发可以是读操作完成或写完成。 //我们不知道为什么完成:读?写?或两者都是?
10.5.2 触发事件内核对象
(1)在每个I/O请求的OVERLAPPED结构体的hEvent创建一个用来监听该请求完成的事件对象。当一个异步I/O请求完成时,设备驱动程序会调用SetEvent来触发事件。
(2)驱动程序仍然会像从前一样,将设备对象也设为触发状态,因为己经有了可用的事件对象,所以可以通过SetFileCompletionNoticationModes(hFile,FILE_SKIP_SET_EVENT_ON_HANDLE)来告诉操作系统在操作完成时,不要触发文件对象。
(3)以下代码是故意那样设计的。实际应用中,可用一个循环来等待I/O请求完成。
【示例程序】——利用事件对象处理多个IO请求
//以异步方问访问hFile设备(传入参数FILE_FLAG_OVERLAPPED) HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...); //异步I/O读请求 BYTE bReadBuffer[10]; OVERLAPPED oRead = { 0 }; oRead.Offset = 0; oRead.hEvent = CreateEvent(...); //创建一个监听读的事件对象 ReadFile(hFile, bReadBuffer, 100, NULL, &o); //添加异步I/O读请求 //异步I/O写请求 BYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; OVERLAPPED oWrite = { 0 }; oWrite.Offset = 10; oWrite.hEvent = CreateEvent(...);//创建一个监听写的事件对象 WriteFile(hFile, bWriteBuffer, _countof(bWriteBuffer), NULL, &oWrite); ... HANDLE h[2]; h[0] = oRead.hEvent; h[1] = oWrite.hEvent; DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE); switch (dw-WAIT_OBJECT_0) { case 0: break;//读完成 case 1: break;//写完成 }
10.5.3 可警告(或叫可提醒)I/O
(1)创建线程时,会同时创建一个与线程相关联的APC队列(异步过程调用),可以告诉设备程序驱动程序在I/O完成时,将通知信息添加到线程的APC队列中。可调用ReadFileEx和WriteFileEx函数。
(2)ReadFile/WriteFileEx函数与Read/WriteFile最大的不同在于最后一个参数,这是一个回调函数(也叫完成函数)的地址,当*Ex发出一个I/O请求时,这两个函数会将回调函数的地址传给设备驱动程序。当设备驱动程序完成I/O请求后,会在发出I/O请求的线程的APC队列中添加一项。该项包含了完成函数的地址以及发出I/O请求时使用的OVERLAPPED的地址。
(3)当一个可提醒I/O完成时,设备驱动程序不会去触发OVERLAPPED结构中的hEvent成员。所以这个成员可以为我所用。
(4)完成函数的原型
void WINAPI CompletionRoutine(DWORD dwError,DWORD dwNumBytes,OVERLAPPED* po);
★dwError,dwNumBytes这些参数在OVERLAPPED里都有,这里出现重复是因为以前OVERLAPPED结构并未公开的原因。
(5)添加到APC队列的各项I/O请求,并不一定是按添加的顺序被执行!可以会是任意的顺序来执行。
(6)要让APC队列执行,线程必须通过调用SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectEx、SignalObjectAndWait、GetQueuedCompletionStatusEx、MsgWaitForMultipleObjectEx等函数将自己设为可警告状态。当调用这些函数时,系统会首先检查APC队列,如果队列中至少有一项,那线程不会进入睡眠状态,而是取出APC队列中各项,并调用其回调函数,直至队列为空,然后可警告函数返回。如果调用可警告函数时,APC队列是空的,则线程才会将自己挂起,进入可警告的睡眠状态,当APC队列出现一项或正在等待的那个内核对象被触发或超时,线程被唤醒,然后函数立即返回。
(7)可提醒I/O的优劣
①回调函数:必须创建回调函数,使代码变复杂。而且回调函数不能带额外的信息,使用不得不大量使用全局变量,幸运的是这些回调函数是被同一线程调用的,所以不需要同步。
②线程问题:发出I/O请求的线程必须同时对完成通知进行处理。可能使这个线程负载过大,而其他线程处于空闲状态却无事可做。
(8)手动添加一个到APC队列:QueueUserAPC(pfnAPC,hThread,dwData);//回调函数、线程、额外信息 (可手动添加一项以便唤醒线程,强制其退出)
10.5.4 I/O完成端口
10.5.4.1 I/O完成端口简介
(1)由于受CPU数量的限制,每次可运行线程数量超过CPU的数量是没有意义的,因为那会浪费宝贵的CPU周期在执行线程上下文的切换。
(2)使用I/O完成端口时,要先初始化一个线程池,用少数的几个线程来最大限度的处理客户的请求,而不必为每个客户创建一个服务线程,这样即减少创建和销毁线程的开销,也减少线程上下文切换的次数。
(3)可以把完成端口看成系统维护的一个队列,操作系统把重叠IO操作完成的事件通知放到该队列里,由于是暴露 “操作完成”的事件通知,所以命名为“完成端口”(Completion Ports)。
(4)完成端口是一个内核对象,但是唯一的一个不需要设置安全属性的内核对象!
10.5.4.2 与I/O完成操作有关的3个函数
(1)CreateIoCompletionPort函数
参数 |
描述 |
hFile |
设备句柄 |
hExistingCompletionPort |
已存在的完成端口的句柄,该项是用来将设备关联到完成端口的。 |
CompletionKey |
完成键,见后面详细介绍 |
dwNumberOfConCurrentThreads |
允许并发执行的线程数量,填0时,默认为CPU的数量。 |
返回值 |
成功——I/O完成端口的句柄 失败——NULL,可进一步GetLastError |
注意事项
①该函数逻辑上可分为以下函数
A创建完成端口
HANDLE CreateIOCP(DWORD dwNumberOfConcurrentThreads =0){ return (CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,dwNumberOfConcurrentThreads)); }
B将设备关联到完成端口
BOOL AssociateDeviceWithCompletionPort(HANDLE hIOCP,HANDLE hDevice,dwCompletionKey,0){ HANDLE h = CreateIoCompletionPort(hDevice,hIOCP,dwCompletionKey,0); return (h==hIOCP); }
②为了使用完成端口,创建设备(如CreateFile时)要异步设备,即创建函数的相应参数中要指定为FILE_FLAG_OVERLAPPED
(2)GetQueuedCompletionStatus函数
①函数
参数 |
描述 |
hCompletionPort |
要监听的完成端口 |
pdwNumberOfBytesTransferred |
传回已传传输的字节数 |
pCompletionKey |
传回将设备关联到完成端口时使用的完成键。完成键一般被设计为一个叫“单句柄数据”的结构体(PER_HANDLE_DATA),用来标识是I/O完成项是哪个设备操作己经完成。这个结构体应为全局变量或堆在分配的。 |
ppOverlapped |
传回创建设备时使用的IO重叠结构。一般被设计为一个叫“单IO数据”的结构体(PER_IO_DATA),该结构体的第1个成员为OVERLAPPED结构体。用来标识是设备的哪种操作(如读或写)。这个结构体应为全局变量或堆在分配的。 |
dwMilliseconds |
等待的时间(毫秒数) |
返回值 |
成功时:非0,并传回第2-4个参数的值、 失败时:0,则有如下几种情况 ①如果*ppOverlapped为空,当GetLastError为WAI_TIMEOUT表示超时,当GetLastError为ERROR_ABANDONED_WAIT_0或其他表示完成端口出现故障。 ②如果*ppOverlapped不为空。当GetLastError为ERROR_OPERATION_ABORTED表示相应的设备取消了I/O异步请求。如果为其他,可能表示设备(如Socket断开了连接)。 |
备注:A、调用该函数时,如果完成队列中己经已完成的I/O项,则调用线程会直接取出I/O完成项,而不会进行等待状态。 B、如果此时完成队列是空的,则调用线程会进入等待状态。 |
②扩展版本GetQueuedCompletionStatusEx:一次取出多个I/O请求的结果来处理。
参数 |
描述 |
hCompletionPort |
要监听的完成端口 |
pCompletionPortEntries |
从I/O完成队列中取出各项,复制信息到该数组中。数组中的每个元素都是一个OVERLAPPED_ENTRY结构。 |
ulCount |
最多可以复制多少项到数组 |
pNumEntriesRemoved |
从完成队列中实际取回已完成的I/O项的数量 |
dwMilliseconds |
等待的时间(毫秒数) |
bAltertable |
FALSE表示函数会一直等待一个已完成I/O请求被添加到完成队列直到超时。 TRUE:表示当队列中没有已完成的I/O项时,线程将进入可警告状态。 |
备注:因异步IO可能会以同步方式完成(由于高速缓存的存在),但系统仍然完成通知添加到完成队列中,为了略微提高性能,可调用SetFileCompletionNotificationModes函数并传入FILE_SKIP_COMPLETION_PORT_ON_SUCCESS来告诉Windows当以同步方式完成异步I/O请求时,不要将完成通知添加到完成队列中。 |
(3)PostQueuedCompletionStatus函数——模拟投递已完成的I/O给完成端口
参数 |
描述 |
hCompletionPort |
要监听的完成端口 |
dwNumBytes |
指定—个值,直接传递给GetQueuedCompletionStatus函数中对应的参数 |
CompletionKey |
完成键,传递给GetQueuedCompletionStatus函数相应的参数。 |
pOverlapped |
I/O重叠结构体,传递给GetQueueCompletionStatus函数相应的参数。 |
备注:①可以通过多次PostQueuedCompletionStatus来通知线程池中的每个线程进行清理工作并正常退出,因为线程如果正在退出,就不会再次调用GetQueueCompletionStatus。 ②因线程是后进先出地被唤醒,所以如果要让每个线程都有机会得到模拟的I/O项,时这里需要用其他线程同步机制,否则同一个线程可能多次得到相同的通知。 ③在Vista以上,调用CloseHandle(hIOCP),系统会将所有正在等待GetQueueCompletionStatus的线程唤醒,并返回FALSE。此时调用如果这些线程调用GetLastError时将返回ERROR_INVALID_HANDLE,可以通过这种方式通知自己应该得体地退出了。 |
10.5.4.3 I/O完成端口的内部运行原理及周边架构
(1)I/O完成端口的内部运作
【第1个结构体】设备列表
将设备与I/O完成端口关联时,设备和完成键会被加入I/O完成端口的设备列表中。
【第2个结构体】I/O完成队列
①当设备的一个异步I/O请求完成时,如果该系统与I/O完成端口相关联,那么系统会将已完成的I/O请求追加到该结构体的末尾。有时也可以不让这个完成通知加入到I/O完成队列中,方法是在发出I/O请求时,将OVERLAPPED结构体中的一个有效的hEvent与1进行或运算。如
Overlapped.hEvent = (HANDLE)((DWORD_PTR)hEvent |1); ReadFile(…,&Overlapped); … CloseHandle((hEvent & ~1));//关闭时,要恢复该事件句柄后才关闭。
②该队列中的每一项包含:已传输的字节数、关联时用的完成键、发送I/O请求时的OVERLAPPED结构体的指针及一个错误码。
③该队列按先进先出来的规则来存取。
【第3个结构体】等待线程列表(后进先出)
①当线程池中的每个线程调用GetQueuedCompletionStatus时,调用线程的ID会加到这个队列中。表示哪些线程 当前正在等待完成通知,以便处理。
②当I/O完成端口出现一项时,该完成端口会唤醒其中的一个线程,这个线程将得到这个已完成的I/O项中的所有信息(包括已传输的字节数、完成键及OVERLAPPED的指针)
③等待线程列表中的线程是以后进先出的方式被唤醒。如果某个线程处理完一个已完成的I/O项后,会循环调用GetQueueCompletionStatus,此时如果完成队列中仍有其他完成项且正在运行的线程数量小于允许的最大并发线程数量时,则该线程会直接取走这个I/O完成项而不会进入等待状态,这样可以减少线程上下文切换。否则,该线程会进入等待线程队列。当新出现了另一项已完成的I/O项时,这个线程会再次被唤醒来处理新的项。
④如果驱动程序处理I/O请求很慢,这时I/O完成通知也比较少,使得一个线程可以处理全部已完成的I/O项,而其他线程继续睡眠。当正在等待的线程数量大于已完成的I/O请求的数量时,系统将那些未被调度的多余线程的内存资源(如栈空间)换出内存。
【第4个结构体】已释放线程列表
①让完成端口记住哪些线程已被唤醒,正在处于可被调度的状态
②如果一个已释放的线程调用任何函数使该线程切换到睡眠状态,那么完成端口会将该线程ID从已释放线程列表中删除,并添加到已暂停线程列表。
③完成端口根据CreateIoCompletionPort时指定的并发线程数量,将尽可能多的线程保持在已释放线程列表中。如果一个已释放线程由于任何原因进入等待状态,那么已释放列表会缩减,完成端口就会将等待线程列表中一个线程释放出来,添加到已释放列表,并唤醒这个线程。如果已暂停列表中的线程又恢复运行,就会进入重新进入已释放列表。这意味着在短时间内,已释放列表中的线程数量将大于创建完成端口时指定的最大允许并发的线程数量。当发生这种情况时,完成端口是知道的,在已释放线程数量降低到最大允许并发的线程数量之前,它不会再唤醒其他任何线程,一旦这些线程进入下一个循环并调用GetQueueCompletionStatus,可运行线程的数量会迅速下降。
【第5个结构体】已暂停线程列表
(2)I/O完成端口及周边的架构
【完成端口对线程池的管理】
①允许同时并发运行的线程:即CreateIoCompletionPort中指定的数值,一般设为主机CPU的数量。当已完成的I/O项被加入到完成队列时,I/O完成端口唤醒的线程数量最多不会超过该值(注意,某个时间段里,可能因已暂停线程列表中的线程被唤醒,会出现短暂性的超过这个允许并发数值,但当线程再次调用GetQueueCompletionStatus时,可运行的线程数量会被迅速降下来)。
②外部线程池的线程数量:2*CPU数量或2*CPU数量+2。也就是线程池中线程的数量,一般要高于允许同时并发运行的线程数量,这是因为当那些被唤醒的线程中因某种原因被阻塞时,完成端口可以唤醒其他线程来工作,以充分利用CPU来执行任务。
③让线程退出完成端口的方式:A、线程退出。B、线程调用GetQueueCompletionStatus,并传入另一个不同的I/O完成端口句柄。C、销毁线程当前被指派的I/O完成端口。
【FileCopy示例程序】 使用IO完成端口进行文件的复制
//EnsureCleanup.h
/************************************************************************* Module: EnsureCleanup.h Notices: Copyright(c) 2007 Jeffrey Richter & Christophe Nasarre Purpose: 本类可确保当对象超出作用域后,会被自动释放 *************************************************************************/ /*例如: HANDLE hfile = CreateFile(...); ... CloseHandle(hfile); 您可以编写如下的样子: CEnsureCloseFile hfile = CreateFile(...); */ #pragma once //只编译一次 ////////////////////////////////////////////////////////////////////////// #include "CmnHdr.h" ////////////////////////////////////////////////////////////////////////// //对象释放函数的指针.参数为对象的句柄,使用了UINT_PTR类型可在32和64位上运行 //如CloseHandle(hFile)或RegCloseKey(hkey); typedef VOID(WINAPI* PFNENSURECLEANUP)(UINT_PTR); //函数指针 ////////////////////////////////////////////////////////////////////////// //每个模版的实例需要类型、一个cleanup函数的地址、句柄 template <class TYPE,PFNENSURECLEANUP pfn,UINT_PTR tInvalid=NULL> class CEnsureCleanup{ public: //默认构造函数(假设句柄是无效的,所以不需要清除) CEnsureCleanup(){m_t = tInvalid;} //带参构造函数 CEnsureCleanup(TYPE t) :m_t((UINT_PTR)t){} //析构函数 ~CEnsureCleanup(){ Cleanup(); } //判断句柄是否有效 BOOL IsValid(){ return (m_t != tInvalid); } BOOL IsInvalid(){ return (m_t == tInvalid); } //重载“=”号运算符 TYPE operator =(TYPE t){ Cleanup(); m_t = (UINT_PTR)t; return (*this); } //重载"()"号运算符,并把强制转换为TYPE类型(支持32位和64位) //这个函数会在将该对象被隐式转换为TYPE对象时自动被调用。 operator TYPE(){ return (TYPE)m_t; } //清除句柄 void Cleanup(){ if (IsValid()){ pfn(m_t); //关闭对象 m_t = tInvalid; //不再需要使用该对象了 } } private: UINT_PTR m_t; //对象句柄 }; ////////////////////////////////////////////////////////////////////////// //定义宏,可以更方便的声明指定类型的模版实例 #define MakeCleanupClass(className,tData,pfnCleanup) \ typedef CEnsureCleanup<tData,(PFNENSURECLEANUP)pfnCleanup> className; #define MakeCleanupClassX(className,tData,pfnCleanup,tInvalid) \ typedef CEnsureCleanup<tData,(PFNENSURECLEANUP)pfnCleanup,\ (INT_PTR)tInvalid> className; ////////////////////////////////////////////////////////////////////////// //定义模版类的实例(一些普通C++类) MakeCleanupClass(CEnsureCloseHandle, HANDLE, CloseHandle); //定义CEnsureCloseHandle类 MakeCleanupClassX(CEnsureCloseFile, HANDLE, CloseHandle, INVALID_HANDLE_VALUE); MakeCleanupClass(CEnsureLockFree, HLOCAL, LocalFree);//定义CEnsureLockFree类 MakeCleanupClass(CEnsureGlobalFree, HGLOBAL, GlobalFree);//定义CEnsureGlobalFree类 MakeCleanupClass(CEnsureRegCloseKey, HKEY, RegCloseKey);//定义CEnsureRegCloseKey类 MakeCleanupClass(CEnsureCloseServiceHandle, SC_HANDLE, CloseServiceHandle);//定义CEnsureCloseServiceHandle类 MakeCleanupClass(CEnsureCloseWindowsStation,HWINSTA, CloseWindowStation); MakeCleanupClass(CEnsureCloseDesktop, HDESK, CloseDesktop);//定义CEnsureCloseDesktop类 MakeCleanupClass(CEnsureUnmapViewOfFile, PVOID, UnmapViewOfFile); MakeCleanupClass(CEnsureFreeLibrary, HMODULE, FreeLibrary);//定义CEnsureFreeLibrary类 ////////////////////////////////////////////////////////////////////////// //特殊类的释放 //VirtualFree类的释放,因为需要3个参数 class CEnsureReleaseRegion{ public: CEnsureReleaseRegion(PVOID pv = NULL) :m_pv(pv){} ~CEnsureReleaseRegion(){ Cleanup();} PVOID operator=(PVOID pv){ Cleanup(); m_pv = pv; return (m_pv); } operator PVOID(){ return (m_pv); } void Cleanup(){ if (m_pv !=NULL){ VirtualFree(m_pv, 0, MEM_RELEASE); m_pv = NULL; } } private: PVOID m_pv; }; ////////////////////////////////////////////////////////////////////////// //特殊类:对HeapFree的释放,也是需要3个参数 class CEnsureHeapFree{ public: CEnsureHeapFree(PVOID pv = NULL, HANDLE hHeap = GetProcessHeap()) : m_pv(pv), m_hHeap(hHeap){}; PVOID operator=(PVOID pv){ Cleanup(); m_pv = pv; return (m_pv); } operator PVOID(){ return (m_pv); } void Cleanup(){ if (m_pv!=NULL){ HeapFree(m_hHeap, 0, m_pv); m_pv = NULL; } } private: HANDLE m_hHeap; PVOID m_pv; }; //////////////////////////////////文件结束//////////////////////////////////
//IoCompletionPort.h
/************************************************************************ 模块: IOCP.h 公告: Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre 目的: 封装IO完成端口 ************************************************************************/ #pragma once #include "CmnHdr.h" ////////////////////////////////////////////////////////////////////////// class CIOCP{ public: CIOCP(int nMaxConcurrency = -1){ m_hIOCP = NULL; if (nMaxConcurrency !=-1) (void)Create(nMaxConcurrency); } ~CIOCP(){ if (m_hIOCP != NULL) chVERIFY(CloseHandle(m_hIOCP)); } //参数为0,表示最大并发线程数量与CPU数量一致。 BOOL Create(int nMaxConcurrency = 0){ //创建一个完成端口 m_hIOCP = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, nMaxConcurrency); chASSERT(m_hIOCP != NULL); return (m_hIOCP != NULL); } BOOL AssociateDevice(HANDLE hDevice, ULONG_PTR CompKey){ BOOL fOk = (m_hIOCP == CreateIoCompletionPort(hDevice, m_hIOCP, CompKey, 0)); chASSERT(fOk); return fOk; } //当关闭IOCP句柄,系统会将所有正在等待GetQueueCompleStatus的线程唤醒,并返回 //FALSE给他们,当GetLastError将返回ERROR_INVALID_HANDLE,线程可以获取通知以便退出 BOOL Close(){ BOOL bResult = CloseHandle(m_hIOCP); m_hIOCP = NULL; return bResult; } //向完成端口的队列添加I/O完成通知 BOOL PostStatus(ULONG_PTR CompKey, DWORD dwNumBytes, OVERLAPPED* po, DWORD dwMilliseconds = INFINITE){ BOOL fOk = PostQueuedCompletionStatus(m_hIOCP, dwNumBytes, CompKey, po); chASSERT(fOk); return fOk; } //从完成端口的队列中获得一条I/O请求完成的通知 BOOL GetStatus(ULONG_PTR* pCompKey, PDWORD pdwNumBytes, OVERLAPPED** ppo,DWORD dwMilliseconds=INFINITE){ return (GetQueuedCompletionStatus(m_hIOCP, pdwNumBytes, pCompKey, ppo, dwMilliseconds)); } private: HANDLE m_hIOCP; }; //////////////////////////////文件结束///////////////////////////////////////
//FileCopy.cpp(主文件)
/************************************************************************ Module: FileCopy.cpp Notices: Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre *************************************************************************/ #include "../../CommonFiles/CmnHdr.h" #include "../../CommonFiles/EnsureCleanup.h" #include "../../CommonFiles/IoCompletionPort.h" #include "resource.h" //C运行库头文件 #include <memory.h> #include <tchar.h> ////////////////////////////////////////////////////////////////////////// //每个I/O请求都需要一个OVERLAPPED结构体和一个数据缓冲区 class CIOReq :public OVERLAPPED{ public: CIOReq(){ Internal = InternalHigh = 0; Offset = OffsetHigh = 0; hEvent = NULL; m_nBufferSize = 0; m_pvData = NULL; } ~CIOReq(){ if (m_pvData != NULL) VirtualFree(m_pvData, 0, MEM_RELEASE); } BOOL AllocBuffer(SIZE_T nBufferSize){ m_nBufferSize = nBufferSize; //m_pvData用来作为接收或写入的缓冲区 m_pvData = VirtualAlloc(NULL, m_nBufferSize, MEM_COMMIT, PAGE_READWRITE); return (m_pvData != NULL); } BOOL Read(HANDLE hDevice, PLARGE_INTEGER pliOffSet = NULL){ if (pliOffSet != NULL){ Offset = pliOffSet->LowPart; OffsetHigh = pliOffSet->HighPart; } return (::ReadFile(hDevice, m_pvData, m_nBufferSize, NULL, this)); } BOOL Write(HANDLE hDevice, PLARGE_INTEGER pliOffset = NULL){ if (pliOffset != NULL){ Offset = pliOffset->LowPart; OffsetHigh = pliOffset->HighPart; } return (::WriteFile(hDevice, m_pvData, m_nBufferSize,NULL, this)); } private: SIZE_T m_nBufferSize; PVOID m_pvData; }; ////////////////////////////////////////////////////////////////////////// #define BUFFSIZE (64*1024) //I/O缓冲区的大小64K #define MAX_PENDING_IO_REQS 4 //最大的IO请求数量 //完成键,用来标识是哪种I/O #define CK_READ 1 #define CK_WRITE 2 ////////////////////////////////////////////////////////////////////////// BOOL FileCopy(PCTSTR pszFileSrc, PCTSTR pszFileDst){ BOOL bOk = FALSE; //假设文件复制失败 LARGE_INTEGER liFileSizeSrc = { 0 }, liFileSizeDst; try { { //打开文件(不使用高速缓存,并获得文件大小) CEnsureCloseFile hFileSrc = CreateFile(pszFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, NULL); if (hFileSrc.IsInvalid()) goto leave; //退出 //获取文件大小 GetFileSizeEx(hFileSrc, &liFileSizeSrc); //不使用高速缓存时,I/O请求时,读取和写入的数据量应是扇区大小的整数倍传输 //目标文件的大小应是向上取最接近liFileSizeSrc大小且是BUFFSIZE整数倍 liFileSizeDst.QuadPart = chROUNDUP(liFileSizeSrc.QuadPart, BUFFSIZE); //打开目标文件(不使用高速缓存)并设置文件大小 CEnsureCloseFile hFileDst = CreateFile(pszFileDst, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, //不存在时则创建 FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, hFileSrc);//如果pszFileDst文件存在,则忽略该参数,如果不 //存在则创建,以此为模版并忽略 dwFlagsAndAttributes if (hFileDst.IsInvalid()) goto leave; //退出 //设置文件大小 SetFilePointerEx(hFileDst, liFileSizeDst, NULL, FILE_BEGIN); SetEndOfFile(hFileDst); //创建I/O完成端口 CIOCP iocp(0);//最大并发线程为默认(即与CPU个数一样) iocp.AssociateDevice(hFileSrc, CK_READ); //从源文件中读取 iocp.AssociateDevice(hFileDst, CK_WRITE);//写入目标文件 //初始化变量 CIOReq ior[MAX_PENDING_IO_REQS]; LARGE_INTEGER liNextReadOffset = { 0 }; int nReadsInProgress = 0; int nWritesInProgress = 0; //这是一个小技巧,模拟文件系统写操作完成, //每个对象手动触发写完成事件,让读操作开始工作 for (int nIOReq = 0; nIOReq < _countof(ior);nIOReq++){ //每个I/O请求需要一个数据缓冲区 chVERIFY(ior[nIOReq].AllocBuffer(BUFFSIZE)); nWritesInProgress++; //写的进度 iocp.PostStatus(CK_WRITE, 0, &ior[nIOReq]); } BOOL bResult = FALSE; //只要还有IO操作没完成,就继续监听IO队列请求完成的通知 while ((nReadsInProgress>0) || (nWritesInProgress>0)) { //挂起线程,直到一个I/O请求己经完成 ULONG_PTR CompletionKey; DWORD dwNumBytes; CIOReq* pior; bResult = iocp.GetStatus(&CompletionKey, &dwNumBytes, (OVERLAPPED**)&pior); switch (CompletionKey) { case CK_READ: //读操作完成,开始写入目标文件中 nReadsInProgress--; bResult = pior->Write(hFileDst);//写到与读时相同偏移的位置 nWritesInProgress++; break; case CK_WRITE: //写操作完成,读取源文件 nWritesInProgress--; if (liNextReadOffset.QuadPart<liFileSizeDst.QuadPart){ //如果不是到了文件尾,则从源文件中读取下一个数据块 //因目标文件比源文件大,当读取超过源文件大小的内容时,超出的那部分 //数据会被0替代 bResult = pior->Read(hFileSrc, &liNextReadOffset); nReadsInProgress++; liNextReadOffset.QuadPart += BUFFSIZE; //移动文件指针 } break; } } bOk = TRUE; } leave:; } catch (...){} if (bOk){ //修复目标文件的大小为源文件的大小 CEnsureCloseFile hFileDst = CreateFile(pszFileDst, GENERIC_WRITE, 0, NULL, OPEN_EXISTING,0,NULL); if (hFileDst.IsValid()){ SetFilePointerEx(hFileDst, liFileSizeSrc, NULL, FILE_BEGIN); SetEndOfFile(hFileDst); } } return bOk; } ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam){ chSETDLGICONS(hwnd, IDI_FILECOPY); //禁用“复制按钮”,因为尚没有文件被选择 EnableWindow(GetDlgItem(hwnd, IDOK), FALSE); return TRUE; } void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtrl, UINT codeNotity){ TCHAR szPathName[MAX_PATH]; switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDOK: //将源文件复制到目标文件中 Static_GetText(GetDlgItem(hwnd, IDC_SRCFILE), szPathName, _countof(szPathName)); SetCursor(LoadCursor(NULL, IDC_WAIT)); chMB(FileCopy(szPathName, TEXT("FileCopy.cpy")) ? "文件复制成功" : "复制失败"); break; case IDC_PATHNAME: OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 }; ofn.hwndOwner = hwnd; ofn.lpstrFilter = TEXT("*.*\0"); lstrcpy(szPathName, TEXT("*.*")); ofn.lpstrFile = szPathName; ofn.nMaxFile = _countof(szPathName); ofn.lpstrTitle = TEXT("选择要复制的文件"); ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST; BOOL bOk = GetOpenFileName(&ofn); if (bOk){ //显示源文件在大小 Static_SetText(GetDlgItem(hwnd, IDC_SRCFILE), szPathName); //CEnsureCloseFile类生成的对象会自动释放(见EnsureCleanup.h文件的实现部分) //hFile是CEnsureCloseFile类的对象,下面的语句会先创建一个文件对象,然后调用 //CEnsureCloseFile带参数构造函数来创建一个CEnsureCloseFile对象。(自动进行类型的隐式转换) CEnsureCloseFile hFile = CreateFile(szPathName, 0, 0, NULL, OPEN_EXISTING, 0, NULL); if (hFile.IsValid()){ LARGE_INTEGER liFileSize; //GetFileSizeEx中第1个参数要求是HANDLE类型的,而此处的hFile是CEnsureCloseFile类型的 //要被隐式的转换,而转换能够成功是因为调用了CEnsureCloseFile类里的"()"运算符。 GetFileSizeEx(hFile, &liFileSize); //只显示低32位 SetDlgItemInt(hwnd, IDC_SRCFILESIZE, liFileSize.LowPart, FALSE); } } EnableWindow(GetDlgItem(hwnd, IDOK), bOk); break; } } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return FALSE; } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int){ DialogBox(hInstExe, MAKEINTRESOURCE(IDD_FILECOPY), NULL, Dlg_Proc); return 0; }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 10_FileCopy.rc 使用 // #define IDD_FILECOPY 101 #define IDI_FILECOPY 102 #define IDC_PATHNAME 1001 #define IDC_SRCFILESIZE 1003 #define IDC_SRCFILE 1004 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 103 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1005 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//FileCopy.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_FILECOPY DIALOGEX 0, 0, 315, 45 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "文件拷贝" FONT 8, "MS Shell Dlg", 400, 0, 0x1 BEGIN DEFPUSHBUTTON "路径...",IDC_PATHNAME,9,5,40,14 DEFPUSHBUTTON "复制",IDOK,9,25,40,14 LTEXT "文件大小(字节):",IDC_STATIC,203,27,62,8 LTEXT "0",IDC_SRCFILESIZE,263,27,41,10,SS_SUNKEN EDITTEXT IDC_SRCFILE,58,4,249,14,ES_AUTOHSCROLL | ES_READONLY END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO BEGIN IDD_FILECOPY, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 308 BOTTOMMARGIN, 43 END END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_FILECOPY ICON "FileCopy.ico" #endif // 中文(简体,中国) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED