前言
本文不是全面介绍完成端口的,只是简单介绍了一下完成端口和几个常用概念。本文主要关注完成端口关闭时资源释放问题。
基础介绍
完成端口——可能是Win32下最复杂的一种I/O模型,Win32下最复杂的内核对象。它通过指定数量的线程对重叠I/O请求进行管理,以便为已经完成的I/O请求提供服务,相对其它I/O模型,它管理任意数目I/O套接字。假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能。
通过CreateIoCompletionPort(唯一一个创建内核对象而没有LPSECURITY_ATTRIBUTES参数的Win32函数,这是因为完成端口只应用于进程内)来创建I/O完成端口,当你创建一个I/O完成端口时,内核实际创建了5个不同的数据结构。
- 设备列表。
- I/O完成队列(FIFO)。当一个设备的异步I/O请求完成时,系统检查该设备是否关联了一个完成端口,如果是系统向该完成端口的I/O完成队列加入完成I/O请求项。
- 等待线程队列(LIFO)。当线程池中的一个线程调用GetQueuedCompletionStatus时,调用线程的线程ID备放入该队列中。
- 释放线程队列(活动线程队列)。完成端口通过该队列监视和限定活动线程的数目,这个限定通常是CPU数目,过多的活动线程没有实际意义,它会引发线程切换从而降低性能。
- 暂停线程队列。当线程调用了Sleep、WaitForSingleObject、WaitForMultipleObjects等。
相关概念
工作者线程与完成端口
成功创建一个完成端口后,便可开始将套接字句柄与对象关联到一起。但在关联套接字之前,首先必须创建一个或多个“工作者线程”,以便在I/O请求投递给完成端口对象后,为完成端口提供服务。
完成端口I/O模型的工作流程如下:
1) 通过CreateIoCompletionPort创建完成端口。
2) 创建工作者线程。
3) 通过CreateIoCompletionPort将完成端口与某一设备相关联。
4) 通过WSAXXX发出异步I/O请求。
5) 在工作者线程中通过调用GetQueuedCompetionStatus取得完成I/O请求项进行后续的处理。
单句柄数据和单I/O操作数据
HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);
BOOL GetQueuedCompletionStatus (HANDLE CompletionPort, LPDWORD lpNumberOfBytesTransferred,
PULONG_PTR lpCompletionKey, LPOVERLAPPED *lpOverlapped, DWORD dwMilliseconds );
GetQueuedCompetionStatus的lpCompletionKey参数包含了“单句柄数据”,它是通过调用CreateIoCompletionPort来关联完成端口与设备时,通过CompletionKey参数设定的。也就是说这个数据特定于设备(这里指套接字)。
GetQueuedCompetionStatus的lpOverlapped参数则包含了“单I/O操作数据”,在通过该函数取得I/O完成队列中的I/O请求完成项后,lpOverlapped指向一个对应了发起这个I/O请求时传递的OVERLAPPED数据结构,也就是说这个数据特定于I/O请求。
单句柄数据和单I/O数据有什么用呢?同过单句柄数据我们可以关联特定的处理函数或处理器或其它结构对该句柄之上的I/O进行特定的处理。单I/O数据为异步I/O的发起和完成建立了联系,它可以关联缓冲区或处理器(参见ACE_Proactor),方便异步I/O操作。
需要注意的问题
下面是请求完成通知插入I/O完成队列的几种情况:
- 调用了closesocket
- 调用了CancelIo
- 发起I/O请求的线程终止
- 超时
- PostQueuedCompletionStatus
- I/O请求正常完成
上述情况除正常完成和PostQueuedCompletionStatus外,其他完成通知会使GetQueuedCompletionStatus返回FALSE,而此时lpOverlapped(超时为NULL)指向未完成I/O请求的单I/O数据。明白了这些后,后面讲的大多不是问题,讲一讲加深下印象吧。
资源管理问题一
- 当I/O请求返回非pending错误和GetQueuedCompletionStatus返回FALSE时如果释放单I/O数据占用的资源。(IOCP中的socket错误和资源释放处理方法 )
- 进行重叠I / O操作的同时,强行释放一个OVERLAPPED结构。要想避免出现这种情况,最好的办法是针对每个套接字句柄,调用closesocket函数,任何尚未进行的重叠I / O操作都会完成。
资源管理问题二(关闭完成端口服务)
我们通常通过调用PostQueuedCompletionStatus向I/O完成队列中加入特殊的完成项来结束工作者线程的,此时,对于未完成的I/O请求要分情况处理之:
- 对于在工作者线程中发起的I/O请求(一般情况下是这样),随着该工作者线程的结束这些I/O请求便会完成,那么对于这种情况我们需要另外的线程来做相应的清理工作——通过调用超时参数为0的GetQueuedCompletionStatus函数,遍历I/O完成队列,lpOverlapped包含了特定于I/O操作的数据。
- 也可在收到关闭通知后,关闭套接字或取消相关的操作使得I/O请求完成并处理之。这需要将这些套接字以及相应的I/O操作记录下来。
关于PostQueuedCompletionStatus
由于等待线程队列是LIFO的,所以该函数要想通知每个工作者线程是件棘手的事情。
参考文献
[1]Jeffery Richter.Advanced Windows(3rd Edition),Microsoft Press,1997
[2]Anthony Jones,Jim Ohlund. Network Programming for Microsoft Windows ,Microsoft Press,2002