《Windows核心编程》第10章开头部分一再强调:“IO Completion Port”是“构建高性能、可升缩的应用程序”的最佳设施之一,它不仅适用于处理设备IO,也适用于其它越来越多的应用场景,比如:Job内核对象、Socket编程等。故此,我单独用一篇博客来讲述“IO Completion Port”的应用场景、原理、用法和示例。
一、缘起
1,线程通信
对于一个服务应用程序(service application)来说,典型的操作是对客户请求进行处理,我们无法预见这些客户请求会在何时到达,也无法预见处理这些客户请求需要消耗多少处理器资源。这些操作常常来自诸如网卡、适配器之类的I/O设备,而对这些请求进行处理又经常会用到额外的I/O设备,比如磁盘文件。
在Windows应用程序中,线程是我们最好的工具,可以用来对工作进行划分。我们可以给每个线程指定 一个处理器,这样在多处理器的机器上就可以同时执行多个操作,从而提高吞吐量。当线程发出一个同步设备I/O请求的时候,它会被临时挂起,直到设备完成I/O请求为止。此类挂起会损害性能,这是因为线程无法进行有用的工作,比如开始对另一个客户请求进行处理。因此,简而言之,我们希望线程不会被阻塞住,这样它们就能始终进行有用的工作。
为了不让线程闲下来,我们需要让各个线程就它们正在执行的操作互相通信。这类通信机制可以帮助我们创建高性能的应用程序。
异步I/O实际上也是基于线程通信来实现的,如上图。左边的线程执行正常流程,右边的线程处理缓慢耗时的I/O过程;右边的线程处理完I/O后,会通知左边的线程,而左边的线程在运行到一定时候,也会停下来等待和检查是否有属于它的“完成通知”。
如何管理线程间的通信(或同步),以降低线程的阻塞时间(或等待时间),是设计高性能应用程序的关键。
2,经典服务器模型
回顾历史,我们能够采用以下两种模型之一来构架一个服务应用程序。
1)串行模型(serial model)
一个线程等待一个客户(通常是通过网络)发出请求。当请求到的时候,线程会被唤醒并对客户请求进行处理。
优点:简单,非异步。
缺点:不能同时处理多个请求。
2)并发模型(concurrent model)
一个线程等待一个客户请求,并创建一个新的线程来处理请求。当新线程正在处理客户请求的时候,原来的线程会进入下一个循环并等待另一个客户请求。当处理客户请求的线程完成整个处理过程的时候,该线程就会终止。
优点:能够同时接收多个请求。它相对于“一个等待线程 + 多个处理线程”,等待线程循环运行,随时可以接收客户请求。
比较串行模型和并发模型,可以它们最大的不同是:串行模型服务应用程序只有一线程(Single thread),而并发模型服务应用程序则是有多少个客户请求,就创建多少个线程(one-thread-per-client)。
缺点:一是,并发线程过多时,会增加系统的线程切换时间(context switching),降低CPU的有效工作时间。二是,创建和销毁线程需要一定时间开销。
针对并发模型的两个缺点,微软分别设计了IO完成端口和线程池两种设施,组合使用可以有效替代经典的并发模型。
二、理论
IO完成端口与线程池结合,主要用于创建高效的异步IO应用程序。为了理解IO完成端口的用法,我们需要先了解一些相关知识。
1,重叠结构(OVERLAPPED)
OVERLAPPED的是异步IO必备的一个数据结构。在《Windows核心编程》10.4.1节专门描述它,如下:
When performing asynchronous device I/O, you must pass the address to an initialized OVERLAPPED
structure via the pOverlapped parameter. The word "overlapped" in this context means that the time
spent performing the I/O request overlaps the time your thread spends performing other tasks.
通俗点说就是:
1)OVERLAPPED是ReadFile/WriteFile等异步操作APIs的一个必须的实参(传址方式)。
2)“overlapped”的意思是执行IO请求的时间与线程执行其他任务的时间是重叠的(overlapped)。
再来看MSDN的说明:
Contains information used in asynchronous (or overlapped) input and output (I/O).
链接:https://msdn.microsoft.com/en-us/library/windows/desktop/ms684342(v=vs.85).aspx
我们可以将OVERLAPPED理解为“与某个I/O请求关联的附加信息”。也有人将它理解为I/O请求的ID号,但我觉得“附加信息”更正确一点。事实上,在Jeffrey给的FileCopy.exe示例中,他对OVERLAPPED进行了扩展(继承),如下:
- // Each I/O Request needs an OVERLAPPED structure and a data buffer
- class CIOReq : public OVERLAPPED {
- public:
- CIOReq() {
- Internal = InternalHigh = 0;
- Offset = OffsetHigh = 0;
- hEvent = NULL;
- m_nBuffSize = 0;
- m_pvData = NULL;
- }
- ......
- private:
- SIZE_T m_nBuffSize;
- PVOID m_pvData;
- };
Jeffrey的这个扩展揭示了“异步IO请求”的本质 —— an OVERLAPPED structure and a data buffer。我们真正需要传输的就是这两个东西,其中OVERLAPPED作为标识,buffer才是真实的信息载体。当然,如果是同步IO,一次只能接收一个请求,就无需OVERLAPPED这个标识物啦,直接传NULL即可。
注:ReadFile/WriteFile只有一个buffer,但是DeviceIoControl有两个buffer —— 输入buffer和输出buffer。这个buffer是信息的载体,而不是信息,事实上,ReadFile的发出请求的时候,buffer为空,待请求处理完了,系统会将信息填充到该buffer上。
2,IO完成端口
IO完成端口是一个内核对象,它的作用是管理关联设备(Associated Devices)的异步IO请求的完成通知。这个话比较拗口,通俗点说,IO完成端口会监视与它管理的设备,如果该设备的某次异步IO请求被处理完了,系统会先把完成通知发给IO完成端口,IO完成端口再将收到的完成通知排队,然后去调度等待列表中的线程来最终收尾一个IO请求。
要理解IO完成端口的工作原理,就需要先搞清楚它的3个列表和2个队列,见《Windows核心编程》图10-4。
1)设备列表
包含所有与该IO完成端口关联的设备。
2)完成队列(先入先出)
IO完成端口将收到的系统发给他的IO完成通知存储在完成队列中。
3)等待线程队列(后入先出)
存储可供IO完成端口调度(选择)的线程。IO完成端口会在该队列中选择一个线程了处理一项完成通知。
4)已释放线程列表
被IO完成端口选中并分派了一项IO完成通知的线程。
5)已暂停线程列表
已释放的线程调用了一个函数将自己挂起。也就是说“4)”中某个线程接到了完成通知,并进行了收尾工作,在这个收尾工作中,调用了诸如Sleep函数,将自己挂起了。
从以上的描述可知,IO完成端口维护了多项线程相关的信息,并协调线程共同高效地完成“并发IO请求”。换句话说,IO完成端口是线程通信的调度者。
3,完成通知(Completion Notification)
完成通知与IO请求是相对的。IO请求包含的是发送的信息(OVERLAPPED + buffer),完成通知包含的是返回信息。完成通知具体包含的信息有4项:
1)已传输字节数;
2)完成键值;
3)OVERLAPPED指针;
4)错误码;
三、流程
介绍完了IO完成端口的背景知识和基本原理后,下面开始介绍怎么使用IO完成端口。本文按照Jeffrey的思路来介绍IO完成端口的使用流程,另外还有其他使用流程,可以参考我的博客:EchoAPP 和Socket编程之IO完成端口,都配了流程图,它们的使用相对复杂一点。
Jeffrey为了方便使用,将IO完成端口封装为一个类—— class CIOCP ,它将创建完成端口和绑定设备分为两个步骤,见FileCopy.exe示例。我总结其流程大致如下:
注:
1)新开工作线程的数量一般设计为CPU数量的2倍。可以通过Windows API,“GetSystemInfo”来获取CPU信息,参考《Windows核心编程》第14章的“SysInfo.exe”示例。
2)在线程中开启循环,不让线程函数直接返回,这样可以避免反“复创建和销毁线程”。
四、示例
参考我的博客:EchoAPP 和Socket编程之IO完成端口,前者相对要简单一点,是普通设备IO,后者是Socket编程。
五,知识图片
我用一张思维导出总结了《Windows核心编程》关于完成端口的知识点,如下:
http://blog.csdn.net/Sagittarius_Warrior/article/details/52368970