IOCP详解
简介: IOCP(I/O Completion Port,I/O完成端⼝)是性能最好的⼀种I/O模型。它是应⽤程序使⽤线程池处理异步I/O请求的⼀种机制。IOCP详解
IOCP(I/O Completion Port,I/O完成端⼝)是性能最好的⼀种I/O模型。它是应⽤程序使⽤线程池处理异步I/O请求的⼀种机制。在处理多个并发的异步I/O请求时,以往的模型都是在接收请求是创建⼀个线程来应答请求。这样就有很多的线程并⾏地运⾏在系统中。⽽这些线程都是可运⾏的,Windows内核花费⼤量的时间在进⾏线程的上下⽂切换,并没有多少时间花在线程运⾏上。再加上创建新线程的开销⽐较⼤,所以造成了效率的低下。
Windows Sockets应⽤程序在调⽤WSARecv()函数后⽴即返回,线程继续运⾏。当系统接收数据完成后,向完成端⼝发送通知包(这个过程对应⽤程序不可见)。
应⽤程序在发起接收数据操作后,在完成端⼝上等待操作结果。当接收到I/O操作完成的通知后,应⽤程序对数据进⾏处理。
完成端⼝其实就是上⾯两项的联合使⽤基础上进⾏了⼀定的改进。
⼀个完成端⼝其实就是⼀个通知队列,由操作系统把已经完成的重叠I/O请求的通知放⼊其中。当某项I/O操作⼀旦完成,某个可以对该操作结果进⾏处理的⼯作者线程就会收到⼀则通知。⽽套接字在被创建后,可以在任何时候与某个完成端⼝进⾏关联。
众所皆知,完成端⼝是在WINDOWS平台下效率最⾼,扩展性最好的IO模型,特别针对于WINSOCK
的海量连接时,更能显⽰出其威⼒。其实建⽴⼀个完成端⼝的服务器也很简单,只要注意⼏个函数,了解⼀下关键的步骤也就⾏了。
分为以下⼏步来说明完成端⼝:
0) 同步IO与异步IO
1) 函数
2) 常见问题以及解答
3) 步骤
4) 例程
0、同步IO与异步IO
同步I/O⾸先我们来看下同步I/O操作,同步I/O操作就是对于同⼀个I/O对象句柄在同⼀时刻只允许⼀个I/O操作,原理图如下:
由图可知,内核开始处理I/O操作到结束的时间段是T2~T3,这个时间段中⽤户线程⼀直处于等待状态,
如果这个时间段⽐较短,则不会有什么问题,但是如果时间⽐较长,那么这段时间线程会⼀直处于挂起状态,这就会很严重影响效率,所以我们可以考虑在这段时间做些事情。
异步I/O操作则很好的解决了这个问题,它可以使得内核开始处理I/O操作到结束的这段时间,让⽤户线程可以去做其他事情,从⽽提⾼了使⽤效率。
由图可知,内核开始I/O操作到I/O结束这段时间,⽤户层可以做其他的操作,然后,当内核I/O结束的时候,可以让I/O对象或者时间对象通知⽤户层,⽽⽤户线程GetOverlappedResult来查看内核I/O的完成情况。
1、函数
我们在完成端⼝模型下会使⽤到的最重要的两个函数是:
CreateIoCompletionPort、GetQueuedCompletionStatus
CreateIoCompletionPort 的作⽤是创建⼀个完成端⼝和把⼀个IO句柄和完成端⼝关联起来:
// 创建完成端⼝
HANDLECompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 把⼀个IO句柄和完成端⼝关联起来,这⾥的句柄是⼀个socket 句柄
CreateIoCompletionPort((HANDLE)sClient,CompletionPort, (DWORD)PerHandleData, 0);
其中第⼀个参数是句柄,可以是⽂件句柄、SOCKET句柄。
第⼆个就是我们上⾯创建出来的完成端⼝,这⾥就把两个东西关联在⼀起了。
第三个参数很关键,叫做PerHandleData,就是对应于每个句柄的数据块。我们可以使⽤这个参数在后⾯取到与这个SOCKET对应的数据。
最后⼀个参数给0,意思就是根据CPU的个数,允许尽可能多的线程并发执⾏。
GetQueuedCompletionStatus的作⽤就是取得完成端⼝的结果:
// 从完成端⼝中取得结果
GetQueuedCompletionStatus(CompletionPort,&BytesTransferred, (LPDWORD)&PerHandleData,(LPOVERLAPPED*)&PerIoData, INFINITE)
主观性第⼀个参数是完成端⼝
第⼆个参数是表明这次的操作传递了多少个字节的数据
第三个参数是OUT类型的参数,就是前⾯CreateIoCompletionPort传进去的单句柄数据,这⾥就是前⾯的SOCKET句柄以及与之相对应的数据,这⾥操作系统给我们返回,让我们不⽤⾃⼰去做列表查询等操作了。
第四个参数就是进⾏IO操作的结果,是我们在投递WSARecv / WSASend 等操作时传递进去的,这⾥操作系统做好准备后,给我们返回了。⾮常省事!!
个⼈感觉完成端⼝就是操作系统为我们包装了很多重叠IO的不爽的地⽅,让我们可以更⽅便的去使⽤,下篇我将会尝试去讲述完成端⼝的原理。
2、常见问题和解答
1)什么是单句柄数据(PerHandle)和单IO数据(PerIO)
单句柄数据就是和句柄对应的数据,像socket句柄,⽂件句柄这种东西。
单IO数据,就是对应于每次的IO操作的数据。例如每次的WSARecv/WSASend等等
其实我觉得PER是每次的意思,翻译成每个句柄数据和每次IO数据还⽐较清晰⼀点。
在完成端⼝中,单句柄数据直接通过GetQueuedCompletionStatus 返回,省去了我们⾃⼰做容器去管理。单IO数据也容许我们⾃⼰扩展OVERLAPPED结构,所以,在这⾥所有与应⽤逻辑有关的东西都可以在此扩展。
2)如何判断客户端的断开
我们要处理⼏种情况
a)如果客户端调⽤了closocket,我们就可以这样判断他的断开:
if(0== GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, 。。。)
{
}
if(BytesTransferred == 0)
{
// 客户端断开,释放资源
}
b)如果是客户端直接退出,那就会出现64错误,指定的⽹络名不可再⽤。这种情况我们也要处理的:
if(0== GetQueuedCompletionStatus(。。。))
部门名称大全霸气{
if( (GetLastError() == WAIT_TIMEOUT) ||(GetLastError() == ERROR_NETNAME_DELETED) )
{
/
/ 客户端断开,释放资源
}
}
3)什么是IOCP?
我们已经提到IOCP 只不过是⼀个专门实现⽤来进⾏线程间的通信的技术,和信号量(maphore)相似,因此IOCP并不是⼀个复杂的概念。⼀个IOCP 对象是与多个I/O对象关联的,这些对象⽀持挂起异步IO调⽤。直到⼀个挂起的异步IO调⽤结束为⽌,⼀个访问IOCP的线程都有可能被挂起。
完成端⼝的⽬标是使CPU保持在满负荷状态下⼯作。
4)为什么使⽤IOCP?
使⽤IOCP,我们可以克服”⼀个客户端⼀个线程”的问题。我们知道,这样做的话,如果软件不是运⾏在⼀个多核及其上性能就会急剧下降。线程是系统资源,他们既不是⽆限制的、也不是代价低廉的。
IOCP提供了⼀种只使⽤⼀些(I/O worker)线程去“相对公平地”完成多客户端的”输⼊输出”。线程会⼀直被挂起,⽽不会使⽤CPU时间⽚,直到有事情做完为⽌。
5)IOCP是如何⼯作的?
当使⽤IOCP时,你必须处理三件事情:a)将⼀个Socket关联到完成端⼝;b)创建⼀个异步I/O调⽤; c)与线程进⾏同步。为了获得异步IO调⽤的结果,⽐如哪个客户端执⾏了调⽤,你必须传⼊两个参数:pCompletionKey参数和OVERLAPPED结构。
3、步骤
编写完成端⼝服务程序,⽆⾮就是以下⼏个步骤:
1、创建⼀个完成端⼝
2、根据CPU个数创建⼯作者线程,把完成端⼝传进去线程⾥
3、创建侦听SOCKET,把SOCKET和完成端⼝关联起来
4、创建PerIOData,向连接进来的SOCKET投递WSARecv操作
5、线程⾥所做的事情:
a、GetQueuedCompletionStatus,在退出的时候就可以使⽤PostQueudCompletionStatus使线程退出;
b、取得数据并处理;
4、例程
下⾯是服务端的例程,可以使⽤sunxin视频中中的客户端程序来测试服务端。稍微研究⼀下,也就会对完成端⼝模型有个⼤概的了解了。
实例结果服务器、客户端如下:
/*
完成端⼝服务器
接收到客户端的信息,直接显⽰出来王者荣耀角色
*/
#include"winerror.h"
#include"Winsock2.h"
#pragmacomment(lib, "ws2_32")
#include"windows.h"
#include<iostream>
usingnamespace std;北京邮电大学排名
///宏定义
#define PORT 5050
#define DATA_BUFSIZE 8192
#define OutErr(a) cout << (a) << endl \
<< "出错代码:"<< WSAGetLastError() << endl \
<< "出错⽂件:"<< __FILE__ << endl \
<< "出错⾏数:"<< __LINE__ << endl \
#define OutMsg(a) cout << (a) << endl;
///全局函数定义
///////////////////////////////////////////////////////////////////////
//午饭
// 函数名 : InitWinsock
汇报材料怎么写
/
/ 功能描述 : 初始化WINSOCK
// 返回值 : void
//
///////////////////////////////////////////////////////////////////////小说考点
void InitWinsock()
{
// 初始化WINSOCK
WSADATA wsd;
if( WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
OutErr("WSAStartup()");
}
拥有歌词
}
///////////////////////////////////////////////////////////////////////
//
// 函数名 : BindServerOverlapped
// 功能描述 : 绑定端⼝,并返回⼀个 Overlapped 的ListenSocket // 参数 : int nPort
// 返回值 : SOCKET
//
///////////////////////////////////////////////////////////////////////
SOCKET BindServerOverlapped(int nPort)