1 基本概念设备---windows操作系统上允许通信的任何东西,比如文件、目录、串行口、并行口、邮件槽、命名管道、无名管道、套接字、控制台、逻辑磁盘、物理磁盘等。绝大多数与设备打交道的函数都是CreateFile/ReadFile/WriteFile等。所以我们不能看到**File函数就只想到文件设备。 与设备通信有两种方式,同步方式和异步方式。同步方式下,当调用ReadFile函数时,函数会等待系统执行完所要求的工作,然后才返回;异步方式下,ReadFile这类函数会直接返回,系统自己去完成对设备的操作,然后以某种方式通知完成操作。 重叠I/O----顾名思义,当你调用了某个函数(比如ReadFile)就立刻返回做自己的其他动作的时候,同时系统也在对I/0设备进行你要求的操作,在这段时间内你的程序和系统的内部动作是重叠的,因此有更好的性能。所以,重叠I/O是用于异步方式下使用I/O设备的。 重叠I/O需要使用的一个非常重要的数据结构OVERLAPPED。 完成端口---是一种WINDOWS内核对象。完成端口用于异步方式的重叠I/0情况下,当然重叠I/O不一定非使用完成端口不可,还有设备内核对象、事件对象、告警I/0等。但是完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能。 2 OVERLAPPED数据结构typedef struct _OVERLAPPED { ULONG_PTR Internal; //被系统内部赋值,用来表示系统状态 ULONG_PTR InternalHigh; //被系统内部赋值,传输的字节数 union { struct { //和OffsetHigh合成一个64位 DWORD Offset; //的整数,用来表示从文件头部的多少字节开始 DWORD OffsetHigh;//操作,如果不是对文件I/O来操作,则必须设定为0 }; PVOID Pointer; }; HANDLE hEvent; //如果不使用,就务必设为0,否则请赋一个有效的Event句柄 } OVERLAPPED, *LPOVERLAPPED; 下面是异步方式使用ReadFile的一个例子 OVERLAPPED Overlapped; Overlapped.Offset=345; Overlapped.OffsetHigh=0; Overlapped.hEvent=0; //假定其他参数都已经被初始化 ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped); 这样就完成了异步方式读文件的操作,然后ReadFile函数返回,由操作系统做自己的事情吧 下面介绍几个与OVERLAPPED结构相关的函数 等待重叠I/0操作完成的函数 BOOL GetOverlappedResult ( HANDLE hFile, LPOVERLAPPED lpOverlapped,//接受返回的重叠I/0结构 LPDWORD lpcbTransfer,//成功传输了多少字节数 BOOL fWait //TRUE只有当操作完成才返回,FALSE直接返回,如果操作没有完成,通过调//用GetLastError ( )函数会返回ERROR_IO_INCOMPLETE ); 宏HasOverlappedIoCompleted可以帮助我们测试重叠I/0操作是否完成,该宏对OVERLAPPED结构的Internal成员进行了测试,查看是否等于STATUS_PENDING值。 3 完成端口的内部机制3.1 创建完成端口 完成端口是一个内核对象,使用时他总是要和至少一个有效的设备句柄进行关联,完成端口是一个复杂的内核对象,创建它的函数是: HANDLE CreateIoCompletionPort( IN HANDLE FileHandle, IN HANDLE ExistingCompletionPort, IN ULONG_PTR CompletionKey, IN DWORD NumberOfConcurrentThreads ); 通常创建工作分两步: 第一步,创建一个新的完成端口内核对象,可以使用下面的函数: HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads) { return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads); }; 第二步,将刚创建的完成端口和一个有效的设备句柄关联起来,可以使用下面的函数: bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey) { HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0); return h==hCompPort; }; 说明 1) CreateIoCompletionPort函数也可以一次性的既创建完成端口对象,又关联到一个有效的设备句柄 2) CompletionKey是一个可以自己定义的参数,我们可以把一个结构的地址赋给它,然后在合适的时候取出来使用,最好要保证结构里面的内存不是分配在栈上,除非你有十分的把握内存会保留到你要使用的那一刻。 3) NumberOfConcurrentThreads通常用来指定要允许同时运行的的线程的最大个数。通常我们指定为0,这样系统会根据CPU的个数来自动确定。 创建和关联的动作完成后,系统会将完成端口关联的设备句柄、完成键作为一条纪录加入到这个完成端口的设备列表中。如果你有多个完成端口,就会有多个对应的设备列表。如果设备句柄被关闭,则表中自动删除该纪录。 3.2 完成端口线程的工作原理 完成端口可以帮助我们管理线程池,但是线程池中的线程需要我们使用_beginthreadex来创建,凭什么通知完成端口管理我们的新线程呢?答案在函数GetQueuedCompletionStatus。该函数原型: BOOL GetQueuedCompletionStatus( IN HANDLE CompletionPort, OUT LPDWORD lpNumberOfBytesTransferred, OUT PULONG_PTR lpCompletionKey, OUT LPOVERLAPPED *lpOverlapped, IN DWORD dwMilliseconds ); 这个函数试图从指定的完成端口的I/0完成队列中抽取纪录。只有当重叠I/O动作完成的时候,完成队列中才有纪录。凡是调用这个函数的线程将被放入到完成端口的等待线程队列中,因此完成端口就可以在自己的线程池中帮助我们维护这个线程。 完成端口的I/0完成队列中存放了当重叠I/0完成的结果---- 一条纪录,该纪录拥有四个字段,前三项就对应GetQueuedCompletionStatus函数的2、3、4参数,最后一个字段是错误信息dwError。我们也可以通过调用PostQueudCompletionStatus模拟完成了一个重叠I/0操作。 当I/0完成队列中出现了纪录,完成端口将会检查等待线程队列,该队列中的线程都是通过调用
|