完成端口之个人理解

51 篇文章 0 订阅
 

下文只是对完成端口的简单介绍,有些是自己的理解,可能不太正确。详细内容可以查看《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 while
	return 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操作。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值