0%

IOCP的使用和技术内幕

IOCP wiki

使用CreateIoCompletionPort函数创建IOCP,还可以把socket或文件句柄与IOCP关联起来。
一个线程,第一次调用GetQueuedCompletionStatus函数时,该线程变为关联了该IOCP的线程,直道下述三种情形之一发生:

  • 该线程退出;
  • 该线程调用GetQueuedCompletionStatus函数关联到其他的IOCP;
  • 该IOCP被关闭。

即,一个线程在任何时刻最多关联一个IOCP。

线程调用GetQueuedCompletionStatus函数等待放入IOCP的I/O完成包(completion packet)。IOCP拥有一个线程池。阻塞在IOCP上的线程按照后进先出(LIFO)顺序被释放(这是为了减少线程切换的代价);而一个线程的完成包按照先进先出(FIFO)顺序从IOCP的队列中取走。IOCP有一个最大允许并发的线程数量上限,在CreateIoCompletionPort函数中制定,每次I/O完成包在从队列取走前检查关联与该IOCP且正在并发执行的线程数量是否达到该限。因其他原因(如调用SuspendThread函数)而挂起的线程不算作正在执行的线程。CompletionKey(完成键)一般作为“单句柄数据”的结构体(PER_HANDLE_DATA),用来标识是哪个设备的I/O完成操作己经完成。IO重叠结构(Overlapped)一般作为“单IO数据”的结构体(PER_IO_DATA),该结构体的第1个成员为OVERLAPPED结构体,用来标识是设备的具体哪个操作。

线程可以用PostQueuedCompletionStatus函数在IOCP上放置一个完成包。

IOCP不能跨进程使用。

关闭IOCP之前,必须先关闭关联在该IOCP之上的所有File Handle或socket。

内部结构

Windows中利用CreateIoCompletionPort命令创建完成端口对象时, 操作系统内部为该对象自动创建了5个数据结构,分别是:

  • 设备列表(Device List): 每当调用CreateIoCompletionPort函数时,操作系统会将该设备句柄添加到设备列表中;每当调用CloseHandle关闭了某个设备句柄时,系统会将该设句柄从设备列表中删除
  • IO完成请求队列(I/O Completion Queue-FIFO):当I/O请求操作完成时,或者调用了PostQueuedCompeltionStatus函数时,操作系统会将I/O请求完成包添加到I/O完成队列中。当操作系统从完成端口对象的等待线程队列中取出一个工作线程时,操作系统会同时从I/O完成队列中取出一个元素(I/O请求完成包。
    等待线程队列(WaitingThread List-LIFO):当线程中调用GetQueuedCompletionStatus函数时,操作系统会将该线程压入到等待* 线程队列中。为了减少线程切换,该队列是LIFO。当I/O完成队列非空,且工作线程并未超出总的并发数时,系统从等待线程队列中取出线程,该线程从自身代码的GetQueuedCompletoinStatus函数调用处返回并继续运行。
  • 释放线程队列(Released Thread List):当操作系统从等待线程队列中激活了一个工作线程时,或者挂起的线程重新被激活时,该线程被压入释放线程队列中,也即这个队列的线程处于运行状态。这个队列中的线程有两个出队列的机会:一是当线程重新调用GetQueuedCompeltionStatus函数时,线程被添加到等待线程队列中;二是当线程调用其他函数使得线程挂起时,该线程被添加到挂起线程队列中。
  • 暂停线程队列(Paused Thread List):释放线程队列中的线程被挂起的时候,线程被压入到挂起线程队列中;当挂起的线程重新被唤醒时,从挂起线程队列中取出放入到释放线程队列。

IOCP 浅析

IOCP 实现的基本步骤

那么 IOCP 完成端口模型又是怎样实现的呢?首先我们创建一个完成端口 CreateIOCompletionPort,然后再创建一个或多个工作线程,并指定它们到这个完成端口上去读取数据。再将远程连接的套接字句柄关联到这个完成端口。工作线程调用 getQueuedCompletionStatus 方法在关联到这个完成端口上的所有套接字上等待 I/O 的完成,再判断完成了什么类型的 I/O,然后接着发出 WSASend 和 WSARecv,并继续下一次循环阻塞在 getQueuedCompletionStatus。
具体的说,一个完成端口大概的处理流程包括:

  1. 创建一个完成端口;
1
Port port = createIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, fixedThreadCount());
  1. 创建一个线程 ThreadA;
  2. ThreadA 线程循环调用 GetQueuedCompletionStatus 方法来得到 I/O 操作结果,这个方法是一个阻塞方法;
1
2
3
While(true){
getQueuedCompletionStatus(port, ioResult);
}
  1. 主线程循环调用 accept 等待客户端连接上来;
  2. 主线程 accept 返回新连接建立以后,把这个新的套接字句柄用 CreateIoCompletionPort 关联到完成端口,然后发出一个异步的 Read 或者 Write 调用,因为是异步函数,Read/Write 会马上返回,实际的发送或者接收数据的操作由操作系统去做。
1
2
3
if (handle != 0L) {
createIoCompletionPort(handle, port, key, 0);
}
  1. 主线程继续下一次循环,阻塞在 accept 这里等待客户端连接。
  2. 操作系统完成 Read 或者 Write 的操作,把结果发到完成端口。
  3. ThreadA 线程里的 GetQueuedCompletionStatus() 马上返回,并从完成端口取得刚完成的 Read/Write 的结果。
  4. 在 ThreadA 线程里对这些数据进行处理 ( 如果处理过程很耗时,需要新开线程处理 ),然后接着发出 Read/Write,并继续下一次循环阻塞在 GetQueuedCompletionStatus() 这里。

更多参考

《Windows.Internals.Part.2.6th.Edition》 - CHAPTER 8: I/O System - I/O Completion Ports


本文地址:http://xnerv.wang/iocp-usage-and-inside/