IOCP IO完成端口
一. IO完成端口概念
IO完成端口的出现是为了解决并发模型中可运行线程上下文切换开销过大而出现的。
在《Windows核心编程》的描述中,IO完成端口是Wnidows系统提供的最复杂的内核对象,是一种解决并发IO请求的最佳模型,是用来实现高容量网路服务器的最佳方法。既然是一个对象,那么就直接分析一下操作系统眼中的完成端口的具体定义吧。Windows中利用CreateIoCompletionPort命令创建完成端口对象时,系统内部自动创建了5个相应的数据结构,分别是:设备列表(Device List)、IO完成请求队列(I/O Completion Queue-FIFO)、等待线程队列(WaitingThread List-LIFO)、释放线程队列(Released Thread List)和暂停线程队列(Paused Thread List)。
设备列表 |
ADD |
每当调用CreateIoCompletionPort绑定到某个设备时,系统会将该设备句柄添加到设备列表中;(与完成端口相关联的<设备列表,完成键>对) |
REMOVE |
每当调用CloseHandle关闭了某个设备句柄时,系统会将该设句柄从设备列表中删除; |
|
I/O 完成队列 |
ADD |
当I/O请求操作完成时,或者调用了PostQueuedCompeltionStatus函数时,系统会将I/O请求完成状态添加到I/O完成队列中,该队列是FIFO。 |
REMOVE |
当完成端口从等待线程队列中取出某一个工作线程时,系统会同时从I/O完成队列中取出一个元素。 |
|
等待 线程 队列 |
ADD |
当线程中调用GetQueuedCompletionStatus函数时,系统会将该线程压入到等待线程队列中,该队列是LIFO(为了减少线程切换)。 |
REMOVE |
当I/O完成队列非空,且工作线程并未超出总的并发数时,系统从等待线程队列中取出线程,该线程从GetQueuedCompletoinStatus函数返回开始工作。 |
|
释放 线程 队列 |
ADD |
1)当系统从等待线程队列中激活了一个工作线程时,或者挂起的线程重新被激活时,该线程被压入释放线程队列中 2)当线程重新调用GetQueuedCompeltionStatus函数时,线程被添加到等待线程队列中; |
REMOVE |
当线程调用其他函数使得线程挂起时,该线程被添加到挂起线程队列中。 |
|
挂起 线程 队列 |
ADD |
释放线程队列中的线程被挂起的时候,线程被压入到挂起线程队列中; |
REMOVE |
当挂起的线程重新被唤醒时,从挂起线程队列中取出。 |
1.创建IO完成端口
HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, //文件 设备句柄 __in_opt HANDLE ExistingCompletionPort, //与设备关联的IO完成端口句柄,为NULL时,系统会创建新的完成端口 __in ULONG_PTR CompletionKey, //完成键,用它来区分各个设备。 __in DWORD NumberOfConcurrentThreads //允许运行的最大线程数量,如果传0表示允许并发执行的线程数量等于CPU主机数量(我的本机是4核8线程,计算机将CPU主机数量当作了8) );
这个函数会完成两个任务: 一是创建一个IO完成端口对象,二是将一个设备与一个IO完成端口关联起来。
2.将已完成的IO请求投递到IO完成端口的队列
PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort, //完成端口的句柄
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
后三个参数是为调用了那个GetQueuedCompletionStatus的线程而准备的。
3.GetQueuedCompletionStatus函数在检查IO完成队列里是否有已经完成的IO请求。
BOOL
WINAPI
GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort, //完成端口句柄
_Out_ LPDWORD lpNumberOfBytesTransferred,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED * lpOverlapped,
_In_ DWORD dwMilliseconds //等待时间
);
(1)在等待线程队列中的线程调用GetQueuedCompletionStatus检查IO完成队列里是否有已经完成的IO请求时,如果IO完成队列中存在已完成的IO请求,则GetQueuedCompletionStatus先删除IO完成队列中这个对应的项,然后将线程ID转移到已释放线程列表中(即当前线程属于已释放列表中的一员了)
(2)在已释放列表中的线程调用GetQueuedCompletionStatus检查IO完成队列里是否有已经完成的IO请求时,如果IO完成队列中不再存在已完成的IO请求,则线程ID再次回到等待线程队列中中。(即当前线程属于等待线程队列中的一员了)
二. IO完成端口实现文件拷贝流程。
1.创建IO完成端口,并将源文件,目标文件和端口相关联
2.将一个已完成的IO通知追加到IO完成队列中,(并非真的写只是让下面的代码从 【读操作】开始,执行序列为: 读-写, 读-写, ... ,读-写)
这里的代码是在《windows核心编程》中精简出来的,我也发现了《windows核心编程》源代码中的一个问题——更新OVERLAPPED结构成员,低32位偏移值和高32位偏移值Offset和OffsetHigh时,最后一次更新会无法更新上:
case CK_WRITE: //写入IO操作已经完成,下一步进行读取操作 WritesInProgress--; //当前文件偏移不能超过文件大小 //超过文件大小就break,之后不再进入while循环,ReadsInProgress无法自加一,推出循环 if (ReadOffset.QuadPart < SourceFileDataLength.QuadPart) { // Not EOF, read the next block of data from the source file. v1->Read(SourceFileHandle, &ReadOffset); ReadsInProgress++; ReadOffset.QuadPart += BUFFER_LENGTH; } break; }
每次写入IO操作已经完成,下一步进行读取操作时,read函数中更新了一次OVERLAPPED结构的Offset和OffsetHigh,但实际上!他们的更新值是上一次操作的旧偏移值了!因为ReadOffset.QuadPart += BUFFER_LENGTH;这一句在最后才执行,
文件偏移ReadOffset.QuadPart 的刷新并没有赋值给Offset和OffsetHigh,而是到了下一次循环进来,才赋值上去,已经是陈旧的刷新值了,到了最后一次文件偏移不再小于源文件大小的时候,Read不再被调用,也就无法最后一次更新Offset和OffsetHigh。
IOCompletionPort.h
#pragma once #include <windows.h> #include <iostream> using namespace std; #define CK_READ 0 #define CK_WRITE 1 #define MAX_PENDING_IO_REQUEST 100 #define BUFFER_LENGTH 1024 class CIOCP { public: CIOCP(int NumberOfConcurrentThreads = -1) { m_IOCompletionPortHandle = NULL; if (NumberOfConcurrentThreads != -1) (void) Create(NumberOfConcurrentThreads); } ~CIOCP() { if (m_IOCompletionPortHandle != NULL) { CloseHandle(m_IOCompletionPortHandle); m_IOCompletionPortHandle = NULL; } } BOOL Close() { BOOL IsOk = CloseHandle(m_IOCompletionPortHandle); m_IOCompletionPortHandle = NULL; return IsOk; } BOOL Create(int NumberOfConcurrentThreads = 0) { m_IOCompletionPortHandle = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, NumberOfConcurrentThreads);//允许并发执行的线程数量等于主机的CPU数量 return(m_IOCompletionPortHandle != NULL); } BOOL AssociateDevice(HANDLE DeviceHandle, ULONG_PTR CompletionKey) { BOOL IsOk = (CreateIoCompletionPort(DeviceHandle, m_IOCompletionPortHandle, CompletionKey, 0) == m_IOCompletionPortHandle); return IsOk; } BOOL AssociateSocket(SOCKET SocketObject, ULONG_PTR CompletionKey) { return(AssociateDevice((HANDLE)SocketObject, CompletionKey)); } BOOL PostStatus(ULONG_PTR CompletionKey, DWORD ReturnLength = 0, OVERLAPPED* Overlapped = NULL) { BOOL IsOk = PostQueuedCompletionStatus(m_IOCompletionPortHandle, ReturnLength, CompletionKey, Overlapped); return IsOk; } BOOL GetStatus(ULONG_PTR* CompletionKey, PDWORD ReturnLength, OVERLAPPED** Overlapped, DWORD Milliseconds = INFINITE) { return(GetQueuedCompletionStatus(m_IOCompletionPortHandle, ReturnLength, CompletionKey, Overlapped, Milliseconds)); } private: HANDLE m_IOCompletionPortHandle; }; class CIORequest : public OVERLAPPED { public: CIORequest() { Internal = InternalHigh = 0; Offset = OffsetHigh = 0; hEvent = NULL; m_BufferLength = 0; m_BufferData = NULL; } ~CIORequest() { if (m_BufferData != NULL) VirtualFree(m_BufferData, 0, MEM_RELEASE); } BOOL AllocBuffer(SIZE_T BufferLength) { m_BufferLength = BufferLength; m_BufferData = VirtualAlloc(NULL, m_BufferLength, MEM_COMMIT, PAGE_READWRITE); return(m_BufferData != NULL); } BOOL Read(HANDLE DeviceHandle, PLARGE_INTEGER ReadOffset = NULL) { //更新OVERLAPPED结构成员,低32位偏移值和高32位偏移值(本代码并未使用) if (ReadOffset != NULL) { Offset = ReadOffset->LowPart; OffsetHigh = ReadOffset->HighPart; } return ReadFile(DeviceHandle, m_BufferData, m_BufferLength,NULL, this);//读1024字节 } BOOL Write(HANDLE DeviceHandle, PLARGE_INTEGER WirteOffset = NULL) { //参数WirteOffset为NULL,不走这里 if (WirteOffset != NULL) { Offset = WirteOffset->LowPart; OffsetHigh = WirteOffset->HighPart; } return WriteFile(DeviceHandle, m_BufferData, m_BufferLength, NULL, this);//写1024字节 } private: SIZE_T m_BufferLength; PVOID m_BufferData; };
.cpp
#include "stdafx.h" #include <windows.h> #include <iostream> #include "IOCompletionPort.h" using namespace std; BOOL SeFileCopy(PCTSTR SourceFileFullPathData, PCTSTR DestinationFileFullPathData); int main() { WCHAR SourceFileFullPathData[] = L"ReadMe.txt"; WCHAR DestinationFileFullPathData[] = L"CopyFile.txt"; if (SeFileCopy(SourceFileFullPathData, DestinationFileFullPathData)) { } printf("Input AnyKey To Exit\r\n"); getchar(); return 0; } BOOL SeFileCopy(PCTSTR SourceFileFullPathData, PCTSTR DestinationFileFullPathData) { BOOL IsOk = FALSE; LARGE_INTEGER SourceFileDataLength = { 0 }; LARGE_INTEGER DestinationFileDataLength = { 0 }; HANDLE SourceFileHandle = NULL; HANDLE DestinationFileHandle = NULL; DWORD ReturnLength = 0; try { { SourceFileHandle = CreateFile(SourceFileFullPathData, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, NULL); if (SourceFileHandle == INVALID_HANDLE_VALUE) goto Exit; GetFileSizeEx(SourceFileHandle, &SourceFileDataLength); DestinationFileDataLength.QuadPart = SourceFileDataLength.QuadPart; DestinationFileHandle = CreateFile(DestinationFileFullPathData, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, SourceFileHandle); //新文件将从这个文件中复制扩展属性 if (DestinationFileHandle == INVALID_HANDLE_VALUE) goto Exit; // File systems extend files synchronously. Extend the destination file // now so that I/Os execute asynchronously improving performance. //先把磁盘空间占用起来,以便后面拷贝顺利进行 SetFilePointerEx( DestinationFileHandle, //目标文件句柄 DestinationFileDataLength, //指针将要移动的字节数 NULL, //在pliNewFilePointer参数指向的LARGE_INTEGER结构体中保存文件指针的新值 FILE_BEGIN); //文件指针起始位置为文件起始位置 //将指定文件的物理文件大小设置为文件指针的当前位置。 SetEndOfFile(DestinationFileHandle); //创建IO完成端口,并将文件和端口相关联 CIOCP IoCompletionPortObject(0); //默认线程个数 IoCompletionPortObject.AssociateDevice(SourceFileHandle, CK_READ); // Read from source file IoCompletionPortObject.AssociateDevice(DestinationFileHandle, CK_WRITE); // Write to destination file // Initialize record-keeping variables CIORequest IoRequestObject[MAX_PENDING_IO_REQUEST]; LARGE_INTEGER ReadOffset = { 0 }; int ReadsInProgress = 0; int WritesInProgress = 0; // Prime the file copy engine by simulating that writes have completed. // This causes read operations to be issued. for (int i = 0; i < _countof(IoRequestObject); i++) { // Each I/O request requires a data buffer for transfers IoRequestObject[i].AllocBuffer(BUFFER_LENGTH); WritesInProgress++; //将一个已完成的IO通知追加到IOCP的【完成队列】中 IoCompletionPortObject.PostStatus(CK_WRITE, 0, &IoRequestObject[i]); } //[CK_WRITE 1024][CK_WRITE 1024][CK_WRITE 1024]投递到IO完成队列 BOOL IsOk = FALSE; /************************************************************************/ /* 因为前一次只是往IOCP的完成队列插入了一项【写完成】,而并非真的写 只是让下面的代码从 【读操作】开始, 执行序列为: 读-写, 读-写, ... ,读-写 当每个【读操作】完成时:把缓冲区中的数据写入【目的文件】,并更新【源文件】的偏移量 当每个【写操作】完成时:更新【目的文件】的偏移量, 同时,因为操作序列是写操作在后,因此写操作完成后,根据更新后的【源文件】的偏移量 和【源文件】大小做比较,如果大于等于源文件大小,则说明这是最后一次读取操作,则当下一次 写操作完成时 退出循环。 如果当前【源文件偏移量】没有达到【源文件大小】则再次从【源文件】 中读取数据进缓冲区, /************************************************************************/ // Loop while outstanding I/O requests still exist while ((ReadsInProgress > 0) || (WritesInProgress > 0)) { // Suspend the thread until an I/O completes ULONG_PTR CompletionKey; DWORD ReturnLength = 0; CIORequest* v1; IsOk = IoCompletionPortObject.GetStatus(&CompletionKey, &ReturnLength, (OVERLAPPED**)&v1, INFINITE); switch (CompletionKey) { case CK_READ: //读取IO操作已经完成,下一步进行写入操作 ReadsInProgress--; v1->Write(DestinationFileHandle,NULL); // Write to same offset read from source WritesInProgress++; break; case CK_WRITE: //写入IO操作已经完成,下一步进行读取操作 WritesInProgress--; //当前文件偏移不能超过文件大小 //超过文件大小就break,之后不再进入while循环,ReadsInProgress无法自加一,推出循环 if (ReadOffset.QuadPart < SourceFileDataLength.QuadPart) { // Not EOF, read the next block of data from the source file. v1->Read(SourceFileHandle, &ReadOffset); ReadsInProgress++; ReadOffset.QuadPart += BUFFER_LENGTH; } //else // { //可以将ReadOffset定义为全局,在CIORequest 的成员函数Read()中更新ReadOffset.QuadPart, //再更新Offset,OffsetHigh,这样Offset,OffsetHigh就不会少更新一次了。 //Offset = ReadOffset->LowPart; //OffsetHigh = ReadOffset->HighPart; //} break; } } IsOk = TRUE; } Exit:; if (SourceFileHandle != NULL) { CloseHandle(SourceFileHandle); SourceFileHandle = NULL; } if (DestinationFileHandle != NULL) { CloseHandle(DestinationFileHandle); DestinationFileHandle = NULL; } } catch (...) { } if (IsOk) { //目标文件大小是页面大小的倍数。用缓冲区打开文件以缩小其大小到源文件的大小 HANDLE DestinationFileHandle = CreateFile(DestinationFileFullPathData, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (DestinationFileHandle!=INVALID_HANDLE_VALUE) { //指针设置,源文件的大小 SetFilePointerEx(DestinationFileHandle,SourceFileDataLength, NULL, FILE_BEGIN); SetEndOfFile(DestinationFileHandle); CloseHandle(DestinationFileHandle); DestinationFileHandle = NULL; } } return(IsOk); }