完成端口之个人理解

来源:互联网 发布:淘宝不上传身份证清关, 编辑:程序博客网 时间:2024/04/30 09:27
 

下文只是对完成端口的简单介绍,有些是自己的理解,可能不太正确。详细内容可以查看《Windows核心编程》

完成端口(简称IOCP)是最为复杂的Windows内核对象,同时也是最有效的异步I/O模型。

IOCP完成端口可以绑定一个文件句柄(HANDLE),以对其进行异步I/O。系统负责具体的I/O操作,当完成之后由I/O系统把完成消息(completion packet )通过函数(自动调用)PostQueuedCompletionStatus()传送到完成端口的I/O完成队列中,从而唤醒完成端口对应的工作线程(自己创建)。

关于完成端口的线程池:

工作线程由用户自己创建,并且所有的工作线程应该执行同一个回调函数;

工作线程负责 完成端口在得到I/O系统的完成通知后 的具体处理;

用户创建的工作线程组成完成端口的线程池;

主要函数介绍:

(1)  CreateIoCompletionPort()函数

功能:这个函数完成两个不同的任务:1、创建一个完成端口对象;2、将文件句柄关联到I/O完成端口对象。

一般可分成两个小函数对CreateIoCompletionPort()函数调用进行抽象。

函数原型:

HANDLE WINAPI CreateIoCompletionPort(  __in          HANDLE FileHandle,  __in          HANDLE ExistingCompletionPort,  __in          ULONG_PTR CompletionKey,  __in          DWORD NumberOfConcurrentThreads);

在创建完成端口时,前面的三个参数为固定值:INVALID_HANDLE_VALUE,NULL,0

最后一个参数NumberOfConcurrentThreads表示同时最多有多少线程处于可运行状态。一般选择默认值0,就是允许并发执行的线程数量等于主机的CPU数量,从而可以避免在线程之间切换的开销;

返回值:如果成功,则返回值为完成端口对应的句柄;否则,返回NULL;

在绑定文件句柄到完成端口时,对于参数CompletionKey,系统不会管到底是一个什么样的值,由用户自己负责(可以传递一个数据结构的指针给它,以唯一标识文件句柄)

(2) GetQueuedCompletionStatus()函数

内核机制:在绑定文件句柄到完成端口后,系统会自动为完成端口监听对应文件句柄的I/O操作。当I/O完成之后,系统给完成端口的I/O完成队列中添加一个成员,包括I/O传输的字节数、重叠结构、CompletionKey等信息。

该函数是阻塞函数,它会使完成端口以先入先出的顺序(所以是"Queued")输出一个I/O完成队列。如果完成端口对应的I/O完成队列为空,则调用该函数的线程会处于休眠状态,直到完成端口接收到I/O系统的完成消息(I/O完成队列不为空)时激活,并把信息存储在相应的内存中。

函数原型:

BOOL WINAPI GetQueuedCompletionStatus(  __in          HANDLE CompletionPort,  __out         LPDWORD lpNumberOfBytes,  __out         PULONG_PTR lpCompletionKey,  __out         LPOVERLAPPED* lpOverlapped,  __in          DWORD dwMilliseconds);

获取I/O的内容的方法(非常重要):

这里介绍比较巧妙和隐蔽的一个方法,在书上看到的,我自己想的话可能想不出  ^_^

方法一:首先需要注意到GetQueuedCompletionStatus()函数中的LPOVERLAPPED * lpOverlapped参数,实际是一个指针,所以我们也可以传递其他类型的指针给它,以获取我们想要的信息。实际上OVERLAPPED结构体只存储一些简单的信息,因此我们可以设计一个结构体,使它包含其他的一些信息,如下面的例子:

//扩展重叠结构typedef struct _PER_IO_DATA{OVERLAPPED ol;//重叠结构char buf[BUFFER_SIZE];//数据缓冲区int nOperationType;//操作类型#define OP_READ 1#define OP_WRITE 2#define OP_ACCEPT 3}PER_IO_DATA, * PPER_IO_DATA;

需要注意:

重叠结构一定要放在该结构体的第一个位置,这样OVERLAPPED结构体和PER_IO_DATA结构体的首地址相同,方便进行转换。因为GetQueuedCompletionStatus()函数在获取到的I/O完成队列中存储的是OVERLAPPED结构体的地址,需要进行一个OVERLAPPED结构体到PER_IO_DATA结构体的转换(确实是设计的很巧妙),以获取I/O的内容。

下面是具体步骤:

首先,申请一个全局的内存地址必须使用GlobalAlloc()函数,否则出错)给上面介绍的扩展的重叠结构,在提交I/O申请的时候使用该扩展结构体的ol作为重叠结构,并传递该扩展结构的Buffer给I/O。这样,把存储的信息放在扩展结构体中;

在工作线程中获取该结构体的地址(由于OVERLAPPED结构体是在PER_IO_DATA结构体的首位,所以这两个的地址是相同的!),并将其转为PER_IO_DATA结构体,访问其中的buf变量就可以获取到I/O接收或发送到的消息。

方法二:

放在作为CompletionKey对应的结构体中。这个相对扩展OVERLAPPED结构体更简单,而且不需要把OVERLAPPED结构体放在扩展结构体的首位。

具体的使用步骤与方法一相同。

注意事项:

怕注意不到,单列出来说。

为了获取文档句柄对应的I/O信息,必须申请全局的内存地址,使用GlobalAlloc()函数

HGLOBAL WINAPI GlobalAlloc(  __in          UINT uFlags,  __in          SIZE_T dwBytes);

其中,参数uFlags指定分配内存的类型,此处取值uFlags=GPTR,以分配固定位置、且全部清零的内存块。

完成端口的使用:

主线程:

(1) 创建一个完成端口;

(2) 创建工作线程(一个或多个),并把完成端口作为参数传递给工作线程;

(3) 把文件句柄绑定到完成端口;

(4) 提交文件句柄的I/O到系统(需要使用OVERLAPPED结构体);

工作线程:

(1) 获取完成端口;

(2) 获得完成端口的I/O完成队列,并获取I/O消息以进行相应的消息处理;

(3) 如果需要重复的进行I/O,则继续提交I/O到系统;

示例:

IOCP也主要用于网络通信方面,把套接字绑定到完成端口上,并为之创建线程,以负责完成端口在获取到I/O完成消息之后的消息处理。

下面以一个简单的例子(该例子是Windows网络与通信程序设计中的例子)。

实现功能:服务器端使用完成端口进行接收来自客户端发送过来的TCP消息,并进行显示。

具体步骤:

1:创建一个完成端口CreateIoCompletionPort();

2:创建一个线程A;

3:A线程循环调用GetQueuedCompletionStatus()函数来得到IO操作结果,这个函数是个阻塞函数。

4:主线程循环里调用accept等待客户端连接上来。 

5:主线程里accept返回新连接建立以后,把这个新的套接字句柄用CreateIoCompletionPort()关联到完成端口,然后发出一个异步的WSASend或者WSARecv调用以提交I/O操作,因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收数据的操作由WINDOWS系统去做。

6:主线程继续下一次循环,阻塞在accept这里等待客户端连接。

7:WINDOWS系统完成WSASend或者WSArecv的操作,把结果发到完成端口。

8:A线程里的GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。

9:在A线程里对这些数据进行处理(如果处理过程很耗时,需要新开线程处理),然后接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。

/*IOCPDemo.cpp文件  调试通过*//*注意,使用CompletionKey来存储接收到的信息*/#include "WSAInit.h"#include <stdio.h>#include <windows.h>// 初始化Winsock库CWSAInit theSock;#define BUFFER_SIZE 1024#define OP_READ   1#define OP_WRITE  2#define OP_ACCEPT 3// per-handle数据typedef struct _PER_HANDLE_DATA        {SOCKET s;// 对应的套节字句柄    sockaddr_in addr;// 客户方地址char buf[BUFFER_SIZE];// 数据缓冲区int nOperationType;// 操作类型}PER_HANDLE_DATA, *PPER_HANDLE_DATA;//工作线程,负责I/O完成之后的消息处理DWORD WINAPI ServerThread(LPVOID lpParam){// 得到完成端口对象句柄HANDLE hCompletion = (HANDLE)lpParam;DWORD dwTrans;PPER_HANDLE_DATA pPerHandle;OVERLAPPED *pOverlapped;while(TRUE){// 在关联到此完成端口的所有套节字上等待I/O完成BOOL bOK = ::GetQueuedCompletionStatus(hCompletion, &dwTrans, (PULONG_PTR)&pPerHandle, &pOverlapped, WSA_INFINITE);if(!bOK)// 在此套节字上有错误发生{::closesocket(pPerHandle->s);::GlobalFree(pPerHandle);::GlobalFree(pOverlapped);continue;}// 套节字被对方关闭if(dwTrans == 0 && (pPerHandle->nOperationType == OP_READ || pPerHandle->nOperationType == OP_WRITE)) {::closesocket(pPerHandle->s);::GlobalFree(pPerHandle);::GlobalFree(pOverlapped);continue;}// 通过per-I/O数据中的nOperationType域查看什么I/O请求完成了switch(pPerHandle->nOperationType){case OP_READ:// 完成一个接收请求{pPerHandle->buf[dwTrans] = '\0';printf(pPerHandle-> buf);// 继续投递接收I/O请求WSABUF buf;buf.buf = pPerHandle->buf;buf.len = BUFFER_SIZE;pPerHandle->nOperationType = OP_READ;DWORD nFlags = 0; ::WSARecv(pPerHandle->s, &buf, 1, &dwTrans, &nFlags, pOverlapped, NULL);  }break;case OP_WRITE:// 本例中没有投递这些类型的I/O请求case OP_ACCEPT:break;}//end of switch}//end of whilereturn 0;}void main(){int nPort = 4567;// 创建完成端口对象,创建工作线程处理完成端口对象中事件HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);//创建工作线程::CreateThread(NULL, 0, ServerThread, (LPVOID)hCompletion, 0, 0);    // 创建监听套节字,绑定到本地地址,开始监听SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, 0);SOCKADDR_IN si;si.sin_family = AF_INET;si.sin_port = ::ntohs(nPort);si.sin_addr.S_un.S_addr = INADDR_ANY;::bind(sListen, (sockaddr*)&si, sizeof(si));::listen(sListen, 5);// 循环处理到来的连接while(TRUE){// 等待接受未决的连接请求SOCKADDR_IN saRemote;int nRemoteLen = sizeof(saRemote);SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen);// 接受到新连接之后,为它创建一个per-handle数据,并将它们关联到完成端口对象PPER_HANDLE_DATA pPerHandle =(PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));pPerHandle->s = sNew;memcpy(&pPerHandle->addr, &saRemote, nRemoteLen);pPerHandle->nOperationType = OP_READ;::CreateIoCompletionPort((HANDLE)pPerHandle->s, hCompletion, (ULONG_PTR)pPerHandle, 0);// 投递一个接收请求OVERLAPPED *pol = (OVERLAPPED *)::GlobalAlloc(GPTR, sizeof(OVERLAPPED));WSABUF buf;buf.buf = pPerHandle->buf;buf.len = BUFFER_SIZE;DWORD dwRecv;DWORD dwFlags = 0;::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, pol, NULL);}}

归根到底概括完成端口模型一句话:
我们不停地发出异步的WSASend/WSARecv IO操作,具体的IO处理过程由WINDOWS系统完成,WINDOWS系统完成实际的IO处理后,把结果送到完成端口上(如果有多个IO都完成了,那么就在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断地取出IO操作结果,然后根据需要再发出WSASend/WSARecv IO操作。

原创粉丝点击