《Windows核心编程》之“完成端口”(对所有IO都是如此,不仅仅是对socket)

    《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进行了扩展(继承),如下:

[cpp] view plain copy
 
  1. // Each I/O Request needs an OVERLAPPED structure and a data buffer  
  2. class CIOReq : public OVERLAPPED {  
  3. public:  
  4.    CIOReq() {  
  5.       Internal = InternalHigh = 0;     
  6.       Offset = OffsetHigh = 0;     
  7.       hEvent = NULL;  
  8.       m_nBuffSize = 0;  
  9.       m_pvData = NULL;  
  10.    }  
  11.     ......  
  12. private:  
  13.    SIZE_T m_nBuffSize;  
  14.    PVOID  m_pvData;  
  15. };  


    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

原文地址:https://www.cnblogs.com/findumars/p/8433005.html