关于重叠I/O,参考《WinSock重叠I/O模型》;关于完成端口的概念及内部机制,参考译文《深度探索I/O完成端口》。
完成端口对象取代了 WSAAsyncSelect 中的消息驱动和 WSAEventSelect 中的事件对象,当然完成端口模型的内部机制要比 WSAAsyncSelect 和 WSAEventSelect 模型复杂得多。
IOCP 内部机制如下图所示:
1.创建完成端口
在 WinSock 中编写完成端口程序,首先要调用 CreateIoCompletionPort 函数创建完成端口。其原型如下:
WINBASEAPI HANDLE WINAPI
CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
DWORD CompletionKey,
DWORD NumberOfConcurrentThreads );
第一次调用此函数创建一个完成端口时,通常只关注 NumberOfConcurrentThreads,它定义了在完成端口上同时允许执行的线程数量。一般设为0,表示系统内安装了多少个处理器,便允许同时运行多少个线程为完成端口提供服务。每个处理器各自负责一个线程的运行,避免了过于频繁的线程上下文切换。
hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
这个类比重叠I/O事件通知模型中(WSA)CreateEvent。
然后再调用 GetSystemInfo(&SystemInfo);取得系统安装的处理器的个数 SystemInfo.dwNumberOfProcessors,根据CPU数创建线程池,在完成端口上,为已完成的I/O请求提供服务。一般线程池的规模,即 线程数 = CPU数*2+2。
下面的代码片段演示了线程池的创建。
// 创建线程池,规模为CPU数的两倍
for(int i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)
{
HANDLE ThreadHandle;
// 创建一个工作线程,并将完成端口作为参数传递给它。
if ((ThreadHandle = CreateThread(NULL, 0, WorkerThread, hCompletionPort,
0, &ThreadID)) == NULL)
{
printf(“CreateThread() failed with error %d/n”, GetLastError());
return;
}
// 关闭线程句柄
CloseHandle(ThreadHandle);
}
2.将套接字句柄关联到完成端口
然后需要将一个句柄与已经创建的完成端口关联起来,这里主要指套接字(AcceptSocket),以后针对这个套接字的I/O完成状态交由完成端口通知,程序接到完成通知后做善后处理。
这需要再次调用 CreateIoCompletionPort 函数(囧)。参数四 NumberOfConcurrentThreads 依旧填0,参数一一般就是 AcceptSocket,参数二为上面创建的完成端口 hCompletionPort。参数三即“完成键”,一般存放套接字句柄的背景信息,也就是所谓的“单句柄数据”。之所以把它叫作“单句柄数据”,因为它是用来保存参数一套接字句柄的关联信息。一般可简单定义如下:
typedef struct {
SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
下面的代码片段演示了每次 Accept 返回时,调用 CreateIoCompletionPort 使返回的 AcceptSocket 与完成端口关联,并传递一个 PerHandleData。
AcceptSocket = WSAAccept(Listen, NULL, NULL, NULL, 0);
PerHandleData->Socket = AcceptSocket;
CreateIoCompletionPort((HANDLE) AcceptSocket, hCompletionPort, (DWORD) PerHandleData, 0);
这个类比重叠I/O事件通知模型中设置(WSA)OVERLAPPED结构中的 hEvent 字段,使一个事件对象句柄同一个文件/套接字关联起来。
3.投递重叠I/O请求
将套接字句柄与一个完成端口关联在一起后,便可以套接字句柄为基础,投递发送与接收请求,开始对I/O请求的处理。接下来,可开始依赖完成端口,来接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Win32重叠I/O机制。在这种机制中,像 WSARecv()和 WSASend()这样的 WinSock API 调用会立即返回。此时,需要由我们的应用程序负责在以后的某个时间,通过一个OVERLAPPED结构,来接收调用的结果。
(WSA)OVERLAPPED 扩展结构的一种定义如下:
typedef struct{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
DWORD BytesSEND;
DWORD BytesRECV;
}OVERLAPPEDPLUS,PER_IO_OPERATION_DATA,*LPPER_IO_OPERATION_DATA;
这里的最后两个参数 BytesSEND 和 BytesRECV 与 GetQueuedCompletionStatus 函数返回时的 ByteTransfered 参数一起同步收发操作。
一般在调用 CreateIoCompletionPort 将套接字句柄与完成端口 hCompletionPort 关联后,还需要为 AcceptSocket 创建 PerIOData,以便为后面调用 WSARecv()/WSASend() 提供(WSA)OVERLAPPED结构和缓冲区。
为确保单I/O数据的生存周期延续到I/O完成,一般动态分配,待I/O完成再回收。对于I/O频繁的系统,则可以使用内存池,每次只是回收到空闲池,最后再真正释放。这样,可避免频繁的内存分配。可参考《MFC基于CPlex结构的内存池化管理》。
下面的是Accept返回,调用CreateIoCompletionPort之后的代码片段。
ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
PerIoData->BytesSEND = 0;
PerIoData->BytesRECV = 0;
PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;
然后调用 WSARecv,投递一个等待接收数据的I/O请求:
WSARecv(AcceptSocket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags, &(PerIoData->Overlapped), NULL);
注意参数一、参数二和参数六,实际上完成了每个 AcceptSocket 与 PerIoData 的捆绑。
4.获取完成通知
在完成端口模型中,工作者线程 WorkerThread 需要调用 GetQueuedCompletionStatus 函数,尝试在完成端口上获取完成通知包。
GetQueuedCompletionStatus函数原型如下:
WINBASEAPI BOOL WINAPI
GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
LPDWORD lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds );
When you perform an input/output operation with a file handle that has an associated input/output completion port, the I/O system sends a completion notification packet to the completion port when the I/O operation completes. The completion port places the completion packet in a first-in-first-out queue. The GetQueuedCompletionStatus function retrieves these queued completion packets. —MSDN
这个类比重叠I/O事件通知模型中的WSAWaitForMultipleEvents/WSAGetOverlappedResult获得I/O操作结果。
参数一为创建线程池时传递的完成端口句柄 hCompletionPort,参数二提供一个DWORD指针,用来接收当I/O完成时实际传输的字节数。
参数三即第二次调用 CreateIoCompletionPort 时传入的单句柄完成键,这里用于确定与 CompletionPort 绑定的具体哪个(套接字)句柄完成了I/O操作导致该函数返回。
参数四即第二次调用 CreateIoCompletionPort 时传入的(套接字)句柄(AcceptSocket)投递重叠I/O请求(WSARecv/WSASend)时指定的 (WSA)OVERLAPPED 结构。实际操作中往往提供一个(WSA)OVERLAPPED 扩展结构,这就是常说的“单I/O数据”。
由于调用 CreateIoCompletionPort 将套接字句柄与完成端口hCompletionPort关联起来了,所以针对 AcceptSocket 这个套接字句柄上的I/O请求(WSARecv)完成时,一个完成通知包将被投递到完成端口 hCompletionPort 消息队列中。GetQueuedCompletionStatus 函数是用来获取排队完成状态,它使调用线程挂起,直到收到一个完成通知包才返回。
If the function dequeues a completion packet for a successful I/O operation from the completion port, the return value is nonzero. The function stores information in the variables pointed to by the lpNumberOfBytesTransferred, lpCompletionKey, and lpOverlapped parameters.
If *lpOverlapped is NULL and the function does not dequeue a completion packet from the completion port, the return value is zero. The function does not store information in the variables pointed to by the lpNumberOfBytesTransferred and lpCompletionKey parameters. —MSDN
在工作者线程WorkerThread中调用GetQueuedCompletionStatus:
while(TRUE)
{
GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE)
if (BytesTransferred == 0) // 出错
{
printf(“Closing socket %d/n”, PerHandleData->Socket);
if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)
{
printf(“closesocket() failed with error %d/n”, WSAGetLastError());
return 0;
}
GlobalFree(PerHandleData);
GlobalFree(PerIoData);
continue;
}
// 根据lpNumberOfBytesTransferred, lpCompletionKey, and lpOverlapped参数进行处理
// ……
}
给GetQueuedCompletionStatus传递的参数三将PerIOData强制转换为(LPOVERLAPPED *) 结构,后面又要配合使用PerIOData的其他字段,这体现了“扩展”二字的用意。
若 GetQueuedCompletionStatus 返回 FALSE,可以调用 (WSA)GetOverlappedResult/(WSA)GetLastError 获知具体错误。
5.完成端口流程小结
如前面所言,完成端口模型利用了Win32重叠I/O机制,它是在利用完成端口队列对象来管理线程池。下面总结一下编写基于完成端口的Winsock服务器程序的要点。
(1)首先,当然要调用CreateIoCompletionPort创建一个完成端口,一般一个应用程序只创建一个完成端口。
(2)然后,创建一个线程池,把完成端口作为参数传给线程参数,以使工作线程调用GetQueuedCompletionStatus在完成端口上等待I/O完成,收到完成通知后提供I/O数据处理服务。
(3)每当Accept(Ex)成功返回后,调用CreateIoCompletionPort将AcceptSocket与完成端口关联起来,并传递AcceptSocket的上下文信息(即“单句柄数据”)给完成键参数。同时为AcceptSocket创建一个I/O缓冲区(即“单I/O数据”,扩展OVERLAPPED结构)。
(4)接着,AcceptSocket调用异步I/O操作函数,如WSARecv和WSASend,抛出重叠的I/O请求。这时需要将单I/O数据的第一个字段—OVERLAPPED结构—传递给WSARecv和WSASend,以表示它们投递的是“重叠”的I/O请求,需要等待系统的I/O完成通知。
(5)至此,当上一步抛出的重叠I/O操作完成时,完成端口上会有一个完成通知包,工作线程收到完成通知,从GetQueuedCompletionStatus返回。通过完成键即单句柄数据提供的客户套接字上下文信息、重叠结构参数以及实际I/O的字节数,就可以正式提供I/O数据服务了。
简言之,涉及两个重要的数据结构:“单句柄数据”和“单I/O数据”(扩展的OVERLAPPED结构);涉及两个重要的API: CreateIoCompletionPort和GetQueuedCompletionStatus;当然,不要忘记重叠请求的投递者WSARecv和WSASend,它们是导火索—通信程序的本质工作就是“通信”。
因为完成端口模型本质上利用了Win32重叠I/O机制,故(扩展的)OVERLAPPED结构提供的沟通机制依然是数据通信重要的线索。另外,要理解完成端口内部机制和工作原理及其在通信中的作用。
6.完成端口应用
下面补充完成端口的项目应用实例。
Windows下的IIS采用了完成端口模型,参考《完成端口与高性能服务器程序开发》、《I/O完成端口(Windows核心编程)》、《A simple application using I/O Completion Ports and WinSock》。
Apache Httpd/httpd/server/mpm/winnt/child.c中的winnt_accept()(AcceptEx)和winnt_get_connection()使用了完成端口ThreadDispatchIOCP,但并没有关联套接字,而是自己构造完成包(mpm_post_completion_context→PostQueuedCompletionStatus),完成键为枚举io_state_e,单句柄为PCOMP_CONTEXT。
// Apache Httpd/httpd/server/mpm/winnt/mpm_winnt.h
typedef enum {
IOCP_CONNECTION_ACCEPTED = 1,
IOCP_WAIT_FOR_RECEIVE = 2,
IOCP_WAIT_FOR_TRANSMITFILE = 3,
IOCP_SHUTDOWN = 4
} io_state_e;
Apache源码中只使用到IOCP_CONNECTION_ACCEPTED和IOCP_SHUTDOWN两种状态。除此之外,Apache里面没有真正的完成端口成分,ntmpm似乎依然是线程池加进程池来处理。具体I/O过程参考Apache源码Apache Httpd/httpd-2.2.15/srclib/apr/file_io/win32/readwrite.c和Apache Httpd/httpd/srclib/apr/network_io/win32/sendrecv.c。
Nginx是由Igor Sysoev为俄罗斯访问量第二的Rambler.ru站点开发的,其特点是占有内存少,并发能力强,Nginx的并发能力确实在同类型的网页伺服器中表现较好。新浪、网易、腾讯、迅雷、CSDN等大型网站都采用了Nginx。Nginx每一个客户端请求进来以后会通过事件处理机制,在Linux是Epoll,在FreeBSD下是KQueue放到空闲的连接里。相关源码参考nginx/src/event/modules下的ngx_epoll_module.c、ngx_kqueue_module.h(c)。参考《基于IO完成端口与WSAEventSelect的nginx事件处理模块:ngx_iocp_module》、《Windows下完成端口移植Linux下的epoll》。
参考:
《Network Programming for Microsoft Windows》 Anthony Jones,Jim Ohlund
《Windows Internals》 Mark E. Russinovich,David A. Solomon
《Windows 2000 Systems Programming Black Book》 Al Williams
《Multithreading Applications in Win32》 Jim Beveridge,Robert Wiener.
《Windows网络与通信程序设计》 王艳平
《Write Scalable Winsock Apps Using Completion Ports》
《Design Issues When Using IOCP in a Winsock Server》