完成端口之个人理解

来源:互联网 发布:ubuntu 设置ntp客户端 编辑:程序博客网 时间:2024/04/30 13:22
下文只是对完成端口的简单介绍,有些是自己的理解,可能不太正确。详细内容可以查看《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结构体只存储一些简单的信息,因此我们可以设计一个结构体,使它包含其他的一些信息,如下面的例子:

[cpp] view plain copy
  1. //扩展重叠结构  
  2. typedef struct _PER_IO_DATA  
  3. {  
  4.     OVERLAPPED ol;          //重叠结构  
  5.     char buf[BUFFER_SIZE];  //数据缓冲区  
  6.     int nOperationType;     //操作类型  
  7.   
  8.     #define OP_READ 1  
  9.     #define OP_WRITE 2  
  10.     #define OP_ACCEPT 3  
  11. }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()这里。

[cpp] view plain copy
  1. /*IOCPDemo.cpp文件  调试通过*/  
  2. /*注意,使用CompletionKey来存储接收到的信息*/  
  3. #include "WSAInit.h"  
  4. #include <stdio.h>  
  5. #include <windows.h>  
  6.   
  7. // 初始化Winsock库  
  8. CWSAInit theSock;  
  9.   
  10. #define BUFFER_SIZE 1024  
  11. #define OP_READ   1  
  12. #define OP_WRITE  2  
  13. #define OP_ACCEPT 3  
  14.   
  15. // per-handle数据  
  16. typedef struct _PER_HANDLE_DATA          
  17. {  
  18.     SOCKET s;               // 对应的套节字句柄      
  19.     sockaddr_in addr;       // 客户方地址  
  20.     char buf[BUFFER_SIZE];  // 数据缓冲区  
  21.     int nOperationType;     // 操作类型  
  22. }PER_HANDLE_DATA, *PPER_HANDLE_DATA;  
  23.   
  24. //工作线程,负责I/O完成之后的消息处理  
  25. DWORD WINAPI ServerThread(LPVOID lpParam)  
  26. {  
  27.     // 得到完成端口对象句柄  
  28.     HANDLE hCompletion = (HANDLE)lpParam;  
  29.     DWORD dwTrans;  
  30.     PPER_HANDLE_DATA pPerHandle;  
  31.     OVERLAPPED *pOverlapped;  
  32.     while(TRUE)  
  33.     {  
  34.         // 在关联到此完成端口的所有套节字上等待I/O完成  
  35.         BOOL bOK = ::GetQueuedCompletionStatus(hCompletion, &dwTrans, (PULONG_PTR)&pPerHandle, &pOverlapped, WSA_INFINITE);  
  36.         if(!bOK)        // 在此套节字上有错误发生  
  37.         {  
  38.             ::closesocket(pPerHandle->s);  
  39.             ::GlobalFree(pPerHandle);  
  40.             ::GlobalFree(pOverlapped);  
  41.             continue;  
  42.         }  
  43.   
  44.         // 套节字被对方关闭  
  45.         if(dwTrans == 0 && (pPerHandle->nOperationType == OP_READ || pPerHandle->nOperationType == OP_WRITE))   
  46.         {  
  47.             ::closesocket(pPerHandle->s);  
  48.             ::GlobalFree(pPerHandle);  
  49.             ::GlobalFree(pOverlapped);  
  50.             continue;  
  51.         }  
  52.   
  53.         // 通过per-I/O数据中的nOperationType域查看什么I/O请求完成了  
  54.         switch(pPerHandle->nOperationType)  
  55.         {  
  56.         case OP_READ:       // 完成一个接收请求  
  57.             {  
  58.                 pPerHandle->buf[dwTrans] = '\0';  
  59.                 printf(pPerHandle-> buf);  
  60.                 // 继续投递接收I/O请求  
  61.                 WSABUF buf;  
  62.                 buf.buf = pPerHandle->buf;  
  63.                 buf.len = BUFFER_SIZE;  
  64.                 pPerHandle->nOperationType = OP_READ;  
  65.                 DWORD nFlags = 0;   
  66.                 ::WSARecv(pPerHandle->s, &buf, 1, &dwTrans, &nFlags, pOverlapped, NULL);    
  67.             }  
  68.             break;  
  69.         case OP_WRITE:      // 本例中没有投递这些类型的I/O请求  
  70.         case OP_ACCEPT:  
  71.             break;  
  72.         }//end of switch  
  73.     }//end of while  
  74.     return 0;  
  75. }  
  76.   
  77. void main()  
  78. {  
  79.     int nPort = 4567;  
  80.   
  81.     // 创建完成端口对象,创建工作线程处理完成端口对象中事件  
  82.     HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);  
  83.   
  84.     //创建工作线程  
  85.     ::CreateThread(NULL, 0, ServerThread, (LPVOID)hCompletion, 0, 0);      
  86.   
  87.     // 创建监听套节字,绑定到本地地址,开始监听  
  88.     SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, 0);  
  89.     SOCKADDR_IN si;  
  90.     si.sin_family = AF_INET;  
  91.     si.sin_port = ::ntohs(nPort);  
  92.     si.sin_addr.S_un.S_addr = INADDR_ANY;  
  93.       
  94.     ::bind(sListen, (sockaddr*)&si, sizeof(si));  
  95.     ::listen(sListen, 5);  
  96.       
  97.     // 循环处理到来的连接  
  98.     while(TRUE)  
  99.     {  
  100.         // 等待接受未决的连接请求  
  101.         SOCKADDR_IN saRemote;  
  102.         int nRemoteLen = sizeof(saRemote);  
  103.         SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen);  
  104.         // 接受到新连接之后,为它创建一个per-handle数据,并将它们关联到完成端口对象  
  105.         PPER_HANDLE_DATA pPerHandle =(PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));  
  106.         pPerHandle->s = sNew;  
  107.         memcpy(&pPerHandle->addr, &saRemote, nRemoteLen);  
  108.         pPerHandle->nOperationType = OP_READ;  
  109.         ::CreateIoCompletionPort((HANDLE)pPerHandle->s, hCompletion, (ULONG_PTR)pPerHandle, 0);  
  110.           
  111.         // 投递一个接收请求  
  112.         OVERLAPPED *pol = (OVERLAPPED *)::GlobalAlloc(GPTR, sizeof(OVERLAPPED));  
  113.   
  114.         WSABUF buf;  
  115.         buf.buf = pPerHandle->buf;  
  116.         buf.len = BUFFER_SIZE;  
  117.           
  118.         DWORD dwRecv;  
  119.         DWORD dwFlags = 0;  
  120.         ::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, pol, NULL);  
  121.     }  
  122. }  

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

原创粉丝点击