介绍
本文是基于windows下异步IO一的后续,上一篇我们讲了关于windows异步io设备访问,包括初始化设备,执行IO设备请求,IO请求完成的通知三个部分,其中完成通知我们说有四种方式,上一篇我们讲了其他的三种,如果有兴趣请先移步上一篇文章。这篇文章我们主要介绍IO完成通知的最后一次方式,IOCP(完成端口)。
创建完成端口
首先完成端口是一个内核对象,有专有的api来创建CreateIoCompletionPort,我们来看下
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
- FileHandle是想要对哪个设备进行IO请求
- ExistingCompletionPort传入已存在的完成端口
- CompletionKey完成是通知的变量,我们自己可以随意填充
- NumberOfConcurrentThreads 允许同一时间运行的最大线程数。传入0,等于电脑的cpu核数
接下来详细讲讲这些相关的参数。
首先我们看ExistingCompletionPort这个参数,大家可能会觉得很奇怪,为什么我创建完成端口还会传入一个已经存在的呢。这要从我们这个api的功能性说起,其实这个api担任了两个功能,第一个创建完成端口,第二绑定相应的设备到完成端口上,根据需要我们把这个api拆成两个部分。
// 创建iocp
m_hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, nMaxConcurrency);
// 关联设备
BOOL fOk = (CreateIoCompletionPort(hDevice, m_hIOCP, CompKey, 0) == m_hIOCP);
首先创建iocp,我们ExistingCompletionPort传入NULL那肯定就是说要创建一个新的iocp了,因为我们目前不绑定设备,那个FileHandle传入INVALID_HANDLE_VALUE,CompletionKey传入0,只传入了一个同时运行线程数,这样我们就创建了一个iocp的内核对象
然后是关联设备,传入设备,完成键,和已有的iocp。这样我们就可以将多个设备关联到同一个iocp上了。
相关数据结构
与iocp相关联的有五个数据结构。
简单说下这个图,基本上都是列表和队列,右边示意是列表或队列的每个元素。
- 设备列表,当我们调用关联设备时CreateIoCompletionPort,就会被添加到这个列表中,在这个设备被关闭时从这个列表中删除,这也是没什么好说的。
- IO完成队列,当我们完成IO设备请求后,系统会检查这个设备是不是与完成端口关联,如果是关联的,会将这个请求的相关数据放到IO完成队列队尾。
- 当我们做IO请求后且IO请求完成到完成队列中,我们怎么能获取到呢?我们需要调用一个api来获取,GetQueuedCompletionStatus。我们看下原型:
WINBASEAPI
BOOL
WINAPI
GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED* lpOverlapped,
_In_ DWORD dwMilliseconds
);
- CompletionPort这个是刚刚完成端口
- 后边三个参数是完成队列中回传回来的数据
- 如果相应的完成端口的完成队列中没有数据,调用线程就会进入阻塞状态,而dwMilliseconds表示等待的超时时间,如果完成队列中有数据,或者超时时间过了这个函数就会返回。
如果调用线程进入等待状态,那么就会添加到等待线程队列。我们也看到完成队列是先入先出,等待线程队列是后进先出。完成队列先入先出很正常,但是为什么等待线程队列要后进先出呢,即比如有多个线程,出现一个IO完成项,最后那个线程对该项处理,完成后进入等待队列,如果有IO完成项,继续唤醒这个线程处理。如果IO项完成的很慢,会不会只有这个线程唤醒执行,那么其他未被调度的线程内存资源就可以换出到磁盘,节省了资源,同时也会减少上下文切换的开销。
- 那大家可能回想,后边那两个数据结构是干嘛的呢,这也是CreateIoCompletionPort这个函数最后一个参数的作用,同时也是IOCP比较智能的地方。大家有没有想如果我们开了多个线程去调用GetQueuedCompletionStatus这个函数,让等待线程队列的数量就会比创建完成端口时的参数NumberOfConcurrentThreads多。这样的话,IOCP也只会让同时唤醒NumberOfConcurrentThreads个线程去处理任务,即使完成队列有任务没有完成,即使等待线程队列还有线程在等待。而正在处理任务的线程就会从等待线程队列移除被放到第四个数据结构(已释放线程列表)中,当已释放线程列表中的一个线程处理任务时自己进入到阻塞状态(比如调用sleep)。那么这个线程就会从已释放线程列表中移除被放入到第五个数据结构中(已暂停线程列表),这时IOCP就会到等待线程队列中唤醒一个线程去执行任务,维持NumberOfConcurrentThreads个线程在运行。当已暂定线程列表中的线程进入运行状态时,从已暂定线程列表移除然后进入到已释放线程列表中,这时会短时间超过NumberOfConcurrentThreads,再当线程调用GetQueuedCompletionStatus进入到等待线程队列,这样就能保证最大运行线程数是NumberOfConcurrentThreads。
PostQueuedCompletionStatus
大家可能看这个和GetQueuedCompletionStatus很像,没错这个调用时就会向完成队列添加一项。一般都用来结束整个过程。比如CompletionKey传一个标识,调用GetQueuedCompletionStatus线程返回发现CompletionKey就会优雅的退出这个线程,回收资源。
WINBASEAPI
BOOL
WINAPI
PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNumberOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
应该比较简单,也没什么要说的。
实例
因为IOCP的高性能适用于网络IO,我写了一个tcp的服务器,所以这一章的实例我们就不用ReadFile和WriteFile来执行IO设备请求。,有专门针对网络IO的api。我们主要讲述IOCP,所以涉及到的网络知识大家自行查找吧,或留言交流。
《windows核心编程》中对IOCP的api进行了封装,我们直接拿来看:
class CIOCP {
public:
CIOCP(int nMaxConcurrency = -1) {
m_hIOCP = NULL;
if (nMaxConcurrency != -1)
(void) Create(nMaxConcurrency);
}
~CIOCP() {
if (m_hIOCP != NULL)
chVERIFY(CloseHandle(m_hIOCP));
}
BOOL Close() {
BOOL bResult = CloseHandle(m_hIOCP);
m_hIOCP = NULL;
return(bResult);
}
BOOL Create(int nMaxConcurrency = 0) {
m_hIOCP = CreateIoCompletionPort(
INVALID_HANDLE_VALUE, NULL, 0, nMaxConcurrency);
chASSERT(m_hIOCP != NULL);
return(m_hIOCP != NULL);
}
BOOL AssociateDevice(HANDLE hDevice, ULONG_PTR CompKey) {
BOOL fOk = (CreateIoCompletionPort(hDevice, m_hIOCP, CompKey, 0)
== m_hIOCP);
chASSERT(fOk);
return(fOk);
}
BOOL AssociateSocket(SOCKET hSocket, ULONG_PTR CompKey) {
return(AssociateDevice((HANDLE) hSocket, CompKey));
}
BOOL PostStatus(ULONG_PTR CompKey, DWORD dwNumBytes = 0,
OVERLAPPED* po = NULL) {
BOOL fOk = PostQueuedCompletionStatus(m_hIOCP, dwNumBytes, CompKey, po);
chASSERT(fOk);
return(fOk);
}
BOOL GetStatus(ULONG_PTR* pCompKey, PDWORD pdwNumBytes,
OVERLAPPED** ppo, DWORD dwMilliseconds = INFINITE) {
return(GetQueuedCompletionStatus(m_hIOCP, pdwNumBytes,
pCompKey, ppo, dwMilliseconds));
}
private:
HANDLE m_hIOCP;
};
这里的功能我们都有讲过,Create创建IOCP,AssociateSocket绑定一个SOCKET,GetStatus封装了GetQueuedCompletionStatus,PostStatus封装了PostQueuedCompletionStatus。
继续看书中也对OVERLAPPED进行了封装,我做了一些修改:
class IOReq : public OVERLAPPED
{
public:
IOReq() {
ResetOverlapped();
}
~IOReq() {}
enum ReqType {
ReqType_Send,
ReqType_Recv,
};
bool Recv(SOCKET socket) {
ResetOverlapped();
ZeroMemory(&(m_Data), sizeof(m_Data));
m_Type = ReqType_Recv;
m_Socket = socket;
m_WSABuffser.buf = m_Data;
m_WSABuffser.len = BUFFER_SIZE;
DWORD recvByte = 0;;
DWORD flag = 0;
int iRet = WSARecv(m_Socket, &m_WSABuffser, 1, &recvByte, &flag, this, NULL);
if (iRet == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
return false;
}
return true;
}
bool Send(SOCKET socket, const char* pData, int len) {
ResetOverlapped();
ZeroMemory(&(m_Data), sizeof(m_Data));
memcpy(m_Data, pData, len);
m_Type = ReqType_Send;
m_Socket = socket;
m_WSABuffser.buf = m_Data;
m_WSABuffser.len = len;
DWORD sendByte = 0;
DWORD flag = 0;
int iRet = WSASend(m_Socket, &m_WSABuffser, 1, &sendByte, flag, this, NULL);
if (iRet == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
return false;
}
return true;
}
SOCKET Socket() const {
return m_Socket;
}
void CloseSocket() {
Utils::CleanupSocket(m_Socket);
}
ReqType Type() const {
return m_Type;
}
const char* data() const {
return m_Data;
}
private:
void ResetOverlapped() {
Internal = InternalHigh = 0;
Offset = OffsetHigh = 0;
hEvent = NULL;
}
private:
char m_Data[BUFFER_SIZE];
WSABUF m_WSABuffser;
SOCKET m_Socket;
ReqType m_Type;
};
IOReq 继承了OVERLAPPED,同时添加了一些相应的数据,包含要接收或者发送的缓存,同时也封装了向socket的发送和接收数据。
接下来我们讲解下关于IOCP的主程序:
void IocpServer::Run()
{
if (Init() < 0) {
return;
}
if (CreateSomeWorkThread() < 0) {
return;
}
AcceptReqAndRecv();
std::size_t size = m_Threads.size();
for (std::size_t i = 0; i < size; ++i) {
m_Threads[i]->join();
}
CleanupAllPendingSocket();
}
void IocpServer::Stop()
{
int size = m_Threads.size();
for (int i = 0; i < size; ++i) {
m_IocpHandle.PostStatus(END_SERVER, -1, NULL);
}
Utils::CleanupSocket(m_ListenSocket);
}
m_IocpHandle是CIOCP的对象,作为IocpServer成员变量。
Init()是初始化网络状态,CreateSomeWorkThread()创建多个线程去调用m_IocpHandle的GetStatus(),然后就是AcceptReqAndRecv接收网络请求和发起recv的IO请求。
Stop函数我们使用了PostStatus函数抛给完成队列完成项,完成项的数量和线程数一致。
继续看下AcceptReqAndRecv(),accept成功后我们需要绑定socket到IOCP上:
// recv
m_IocpHandle.AssociateSocket(acceptSocket, NULL);
IOReq *req = new IOReq;
m_PendingRecvMutex.lock();
m_PendingRecvReqs.emplace(std::make_pair(acceptSocket, req));
m_PendingRecvMutex.unlock();
bool rc = req->Recv(acceptSocket);
if (!rc) {
delete req;
std::cout << "recv one error:" << WSAGetLastError() << std::endl;
}
然后看下我们接收完成项的地方,每个线程都调用DoWork():
void IocpServer::DoWork()
{
ULONG_PTR compleKey;
DWORD numBytes;
IOReq *req = nullptr;
while (1) {
m_IocpHandle.GetStatus(&compleKey, &numBytes, (OVERLAPPED **)&req, INFINITE);
// server end
if (compleKey == END_SERVER) {
return;
}
// the socket disconnect
if (numBytes == 0) {
req->CloseSocket();
ClearPendingSocket(req->Socket());
continue;
}
// complete recv
if (req->Type() == IOReq::ReqType_Recv) {
DoResponse(req->Socket(), req->data(), numBytes);
if (!req->Recv(req->Socket())) {
delete req;
std::cout << "recv one error:" << WSAGetLastError() << std::endl;
}
continue;
}
// complete send
if (req->Type() == IOReq::ReqType_Send) {
ClearPendingSendSocket(req->Socket());
}
}
}
接收到数据后去DoResponse做一些事情。而发送完了就什么也不做。
完
到此我就讲完了IOCP的相关知识,欢迎指正,也欢迎交流,我会把示例代码放到csdn上和github。
github上后期也会继续完善的。
csdn地址: https://download.csdn.net/download/leapmotion/12234038
github地址: https://github.com/zhangdexin/WinIPC