深度探索I/O完成端口

article/2025/9/26 19:00:45

引言

要想编写一个高性能的服务器应用程序,必须实现一个高效的线程模型。让太少或者太多的服务器线程来处理客户的请求,都可能导致性能问题。例如,如果一个服务器创建单个线程来处理所有的请求,那么客户端可能长期等待而得不到响应,因为服务器同一时刻只能忙于处理一个请求。当然单个线程也能并发处理多个请求,当I/O操作被启动时,它可以从一个请求切换到另一个请求,但是这种结构相当复杂,并且不能充分利用多处理器的优势。在另一个极端,服务器可以创建一个大规模的线程池,这样几乎每一个客户请求都可以由一个专门的线程来处理。这种情形通常会导致线程频繁切换:大量线程被唤醒,执行CPU处理,阻塞等待I/O,然后在请求完成之后又一次阻塞以等待新的请求。如果没有别的情况,太多的线程将导致过多的上下文切换,因为调度程序不得不将处理器时间在多个活动线程之间分割。

服务器的目标是使线程避免不必要的阻塞,尽量减少上下文切换。同时,还要使用多线程来发挥最大限度的并行。理想的情况是在每一个处理器上运行一个线程来处理一个客户请求,当处理器上的活动线程完成一个请求时,如果还有其他的请求正在等待,则不阻塞。为了使这一优化处理可以有效的进行,应用程序必须有一种可行的方法,使得一个正在处理客户请求的线程在I/O上阻塞时(例如它在处理过程中需要读取一个文件时)另外一个等待线程被激活。

Windows NT 3.5引进了一系列API使得这个目标的实现变得相对容易。这些API主要聚焦在一个叫完成端口的对象上。在本文中,首先我将讲解完成端口的使用,然后再深入其内部,向你展示Windows NT中完成端口的实现机制。

 

使用I/O完成端口

应用程序将IoCompletion执行体对象当作与多个文件句柄相关的I/O完成的核心。一旦一个文件与一个完成端口相关联,任何在此文件上异步I/O操作的完成都会导致一个完成通知包(completion notification packet)加入到完成端口队列。一个线程只需简单的等待一个完成通知包被排队到此完成端口上,就可以等待在多个文件上的所有正在进行之中的I/O操作的完成事件。Windows API中的WaitForMultipleObjects 提供了类似的功能,但完成端口的优点在于在系统的协助下发挥高效的并发性。这里的并发性可以理解为应用程序主动处理客户请求的线程的数量的多少。

当应用程序创建一个完成端口时,需要设定并发量。该数值指示了在任何给定时候正在运行的与该端口相关联的线程的最大数量。正如前面所提到的,理想情况是在任何给定的时刻,系统中每个处理器都有一个线程在运行。Windows利用与一个端口相关联的并发值参数来控制一个应用程序中活动线程的数量。如果与一个端口相关的活动线程数达到并发值,那么,在这个端口上等待的线程将不允许再运行了。相反,它将等待某个活动线程处理完当前操作并检查是否有别的包正在该端口上等待。如果有的话,该线程只是简单的抓获该包然后处理。在这个过程中,没有上下文切换,CPU得到最大限度的利用。

下图1显示了一个完成端口操作流程的高度图解。客户请求将导致一个I/O包(IRP)被排队到完成端口。操作系统允许不超过并发量上限(即上面提到的那个并发值)的多个线程并发地处理客户端请求。直到一些活动线程因I/O请求而阻塞,等待线程才能被激活。下面我们将做进一步的探讨。

1 I/O完成端口操作流程 

创建完成端口需要调用Windows API CreateIoCompletionPort

HANDLE CreateIoCompletionPort(
  HANDLE FileHandle,
  HANDLE ExistingCompletionPort,
  DWORD CompletionKey,
  DWORD NumberOfConcurrentThreads
);

创建一个完成端口时,通常对参数ExistingCompletionPort赋值NULL NumberOfConcurrentThreads参数定义了在完成端口上同时允许执行的线程数量。如果有文件句柄传递给FileHandle参数,则该文件与完成端口关联在了一起。当这个文件上的I/O请求完成时,一个完成通知包将被投递到完成端口消息队列中。另外一个API GetQueuedCompletionStatus是用来获取排队完成状态,它使调用线程挂起,直到收到一个完成通知包。

BOOL GetQueuedCompletionStatus(
  HANDLE CompletionPort,
  LPDWORD lpNumberOfBytesTransferred,
  LPDWORD CompletionKey,
  LPOVERLAPPED* lpOverlapped,
  DWORD dwMiillisecondTimeout
);

完成端口实际上是在管理一个线程池,它会记录当前活动(即没有被I/O等事件阻塞)的线程数。当有完成通知包到达该端口时,在该端口上等待的线程按照后进先出(LIFO)的次序被唤醒,因此最近(most recently)被阻塞的线程就是获得下一个完成通知包的线程。那些长时间得不到响应的的线程的堆栈将会被从内存调到磁盘交换区去等待,当与一个端口关联的线程太多超过了当前的处理能力时,就可以将长时间阻塞的线程占用的内存减到最少。

服务器应用程序往往通过网络端点来接受客户请求,而这些网络端点是由文件句柄来表示的。这样的例子包括Windows Sockets 2(Winsock2)套接字或者命名管道。当服务器创建它的通信端点时,它将这些通信端点与一个完成端口关联起来,并且它的线程通过调用GetQueuedCompletionStatus来等待此端口上进来的完成通知。当一个线程在此完成端口上得到一个I/O完成通知包时,它便不再等待,开始处理I/O结果数据,从而变成一个活动的线程。一个线程在处理过程中可能将阻塞很多次,比如当它需要从磁盘上的文件读取数据时,或者当它需要与其他的线程同步时。Windows NT检测到这些活动,并且识别出该完成端口上至少已经有一个活动线程。因此,当活动线程由于I/O请求而阻塞时,如果在队列中存在一个包,则唤醒另一个正在此完成端口上等待的线程提供处理服务。

微软的指导原则是,将并发值设置成大约等于该系统中处理器的数目。但是要注意,一个完成端口上实际活动线程数量有可能超过设置的并发值。考虑并发值被设置为1的情况,一个客户请求进来了,某个线程因为被调度来处理该请求而变成活动的。下一个请求到达时,正在该端口上等待的另一个线程却不允许执行,因为活动的线程数已经达到了设置的并发上限值。然后,当活动线程需要等待I/O而阻塞时,等待的线程将被激活,当它尚在活动时,上一个线程的I/O完成了,这使得它继续保持活动状态(继续执行数据处理服务)。此刻,一直到两个线程中有一个被阻塞,并发值始终是2,高于设置的并发上限值1。大多数时候,活动线程数将维持在设置的并发限制值上,或者超过一点。

应用程序通过调用PostQueuedCompletionStatus这个API向完成端口投递一个自定义的完成通知包。服务器一般通过该函数发送消息通知线程有外部事件发生,例如需要温和的关机。

 

完成端口内部机制

当传递NULL值给ExistingCompletionPort参数来调用CreateIoCompletionPort来创建完成端口时,将调用同名的NtCreateIoCompletion系统服务。实质上,IoCompletion对象是建立在一个称为队列的内核同步对象基础上。系统创建一个完成端口的同时,在完成端口所分配到的内存中初始化一个队列对象(指向完成端口的指针同时指向了此队列对象,因为队列对象位于完成端口对象内存的开始处)。当一个线程调用CreateIoCompletionPort来创建完成端口时,第四个参数NumberOfConcurrentThreads即为队列的并发值。NtCreateIoCompletion函数将调用KeInitializeQueue系统服务来初始化该端口的消息队列。

当应用程序再次调用CreateIoCompletionPort时,将调用NtSetInformationFile服务来使参数一(文件句柄)与参数二(一个已有的完成端口)关联起来。完成通知包FileCompletionInformation包含的信息:CreateIoCompletionPort的参数二ExistingCompletionPort(已有的完成端口句柄)和参数三CompletionKey(完成键)。NtSetInformationFile通过解引用操作从该文件句柄获得对应的文件对象,并且申请一个记录完成上下文的数据结构。这个数据结构在NTDDK.H定义如下:

typedef struct _IO_COMPLETION_CONTEXT {
  PVOID Port;
  ULONG Key;
} IO_COMPLETION_CONTEXT, *PIO_COMPLETION_CONTEXT;

最后,将调用NtSetInformationFile系统服务设置文件对象中CompletionContext域的值。当一个异步I/O在一个文件对象上完成时,系统内部执行具有I/O管理功能的IopCompleteRequest系统服务,检查文件对象中的CompletionContext域是否为非NULL。如果是,则I/O管理器生成一个完成通知包,通过调用KeInsertQueue系统服务将完成通知包投递到完成端口队列(注意,完成端口对象和队列对象是同义的)。

当一个服务器线程调用GetQueuedCompletionStatus时,它将调用NtRemoveIoCompletion系统服务。在验证参数后,并且将完成端口句柄转换成一个指向该端口的指针后,NtRemoveIoCompletion调用KeRemoveQueue

正如你所看到的,KeRemoveQueueKeInsertQueue是完成端口模型的两个引擎级函数,它们决定阻塞在完成端口上等待I/O完成通知包的线程什么时候被唤醒。在系统内部,队列对象维护了完成端口上当前活动线程的计数值,以及最大的并发活动线程的数量。当一个线程调用KeRemoveQueue并且当前活动线程数大于或等于并发数上限时,那么该线程将被投放到一个阻塞线程队列(按LIFO顺序)中,等待系统调度来获取并处理完成通知包。此线程列表挂在队列对象的外面,线程的控制块数据结构中有一个指针引用了一个与之相关的队列对象;如果这个指针为NULL,则该线程没有与队列关联。

Windows依赖与线程控制块中的队列指针来跟踪和记录那些“由于被阻塞在除了完成端口之外的其他事情上而变成不活动”的线程。那些有可能会导致一个线程阻塞的调度例程(例如KeWaitForSingleObjectKeDelayExecutionThread等等)要检查该线程的队列指针。如果该指针不为NULL,则这些函数调用KiActivateWaiterQueue一个与队列相关的函数,它会递减与该队列相关联的活动线程的计数值。如果计数值递减到小于设置的并发值,并且此时至少有一个完成通知包在该队列中,那么处于该队列的线程列表最前面的那个线程被唤醒,并且把最老的(the oldest)完成通知包交给它处理。相反,无论何时,与一个队列相关联的线程在阻塞之后被唤醒时,调度程序执行KiUnwaitThread函数来增加该队列上活动线程的计数值。

最后,PostQueuedCompletionStatus这个Windows API将调用NtSetIoCompletion服务。该函数只是简单的调用KeInsertQueue将自定义的完成通知包插入到完成端口的队列中。

 

没有公开的

     Windows NT的完成端口API提供了一种易于使用和高效的方法最大限度地发挥服务器的性能——最大限度的减少上下文切换的同时最大限度的提高系统并发量。这些API使我们能够调用I/O管理器和内核提供的一些服务功能。队列对象可以被设备驱动程序调用(这些接口尽管没有公开,但还是很容易查询到的),不过完成端口的API没有提供相关访问功能。但是,如果队列接口被继承,我们完全可以通过编写队列处理程序并通过手动设置CompletionContext的值来模拟完成端口模型。

 

原文

Inside I/O Completion Portshttp://technet.microsoft.com/en-us/sysinternals/bb963891.aspx


http://chatgpt.dhexx.cn/article/3vNnVCsq.shtml

相关文章

Windows中I/O完成端口机制详解

Windows中I/O完成端口机制详解 引言 要想编写一个高性能的服务器应用程序,必须实现一个高效的线程模型。让太少或者太多的服务器线程来处理客户的请求,都可能导致性能问题。例如,如果一个服务器创建单个线程来处理所有的请求,那么…

c++使用完成端口实现服务器的高性能并发

如何使用c,借助完成端口完成大并发服务器的搭建,是今天要讨论的问题,套路如下: 套路总结一下: 创建完成端口 依据CPU核数创建一定数量的线程 线程中不断调用GetQueuedCompletionStatus检查完成端口状态,分别给予处理 创建一个…

C#高性能大容量SOCKET并发完成端口例子(有C#客户端)完整实例源码

遥望星空 好好干,有前途! 博客园首页新随笔联系管理订阅 随笔- 1082 文章- 0 评论- 151 C#高性能大容量SOCKET并发(转) C#高性能大容量SOCKET并发(零):代码结构说明 C#高性能大容量SOCKET并发(一…

完成端口学习笔记(一):完成端口+控制台 实现文件拷贝

最近在整理手里一个项目的后台服务端归档程序,重新梳理了一下有关“完成端口”的知识,发现还是有很多模棱两可的地方,下面记录一下再次学习的点滴,该篇博文还会有后续的补充章节,不知道什么时间会再补充^_^。 IO概念 还…

Socket编程模型之完成端口模型

转载请注明来源:http://blog.csdn.net/caoshiying?viewmodecontents 一、回顾重叠IO模型 用完成例程来实现重叠I/O比用事件通知简单得多。在这个模型中,主线程只用不停的接受连接即可;辅助线程判断有没有新的客户端连接被建立,如…

完成端口(IOCP)详解[2/2](转载)

版权声明:本文为CSDN博主「PiggyXP」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/piggyxp/article/details/6922277 五 使用完成端口的基本流程 说了这么多的废话&a…

Windows io完成端口

Windows 提供一种称为I/O完成端口(I/O Completion Port)机制,能够让I/O的完成处理交由一个专门的线程池来完成,而线程池的线程数量是一个可配置的参数。这种做法将I/O请求的发起动作与完成处理分离到了不同的线程中。 I/O完成端口是内核对象。个人的感觉…

完成端口(Completion Port)详解

http://blog.csdn.net/piggyxp/article/details/6922277 手把手叫你玩转网络编程系列之三 完成端口(Completion Port)详解 ----- By PiggyXP(小猪) 前 言 本系列里完成端口的代码在两年前就已经写好了,但是由于许久没有写东西了,不知该如何提笔&#xf…

完成端口(CompletionPort)详解 - 手把手教你玩转网络编程系列之三

手把手叫你玩转网络编程系列之三 完成端口(Completion Port)详解 ----- By PiggyXP(小猪) 前 言 本系列里完成端口的代码在两年前就已经写好了,但是由于许久没有写东西了,不知该如何提笔,所以这篇文档总是在酝酿之中……酝酿了两年之后&…

树同构-树哈希

树同构-树哈希 题目描述 题解 对于无根树,由于数据范围较小,可以直接以每个点为根dfs一次,维护其树哈希的值,然后用并查集维护 (若数据范围大一些,可以以树的重心跑dfs) 代码实现 #include&…

2.3 树的同构(树,c)

树的同构 树的同构输入格式:输出格式:输入样例1(对应图1):输出样例1:输入样例2(对应图2):输出样例2: 题意理解输入两棵二叉树的信息,判断是否同构(对应图1) 求解思路二叉…

03-树1 树的同构

题目 给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称两棵树是“同构”的。例如图1给出的两棵树就是同构的,因为我们把其中一棵树的结点A、B、G的左右孩子互换后,就得到另外一棵树。而图2就不是同构的。 图1 图2 现给…

『树同构的判定(树Hash)』CF718D:Andrew and Chemistry

题目描述 题解 这道题目的难点在于如何判断树的同构,这就是所谓的树的哈希。 我们假设需要求解以x为根的子树的hash值,我们可以将子树的hash值存储到vector内,排序以后用map来判断重复。这个写法十分简单。具体如下: int dfs(i…

数据结构之树的同构

目录 前言 题意理解 求解思路 二叉树表示 程序框架搭建 读数据建二叉树 二叉树同构判别 前言 本篇主要讲有关二叉树的同构判断。 题意理解 给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则称这两棵树是“同构”的。 例如: 左图…

2020牛客多校10:Identical Trees(树hash + 树同构 + 费用流模板)

题意:给出两棵同构的有根树,同构修改点的标号使得两棵树完全一样,至少需要修改多少次。 分析:肯定是将子树和另外一棵的某个子树对应,而两棵子树的问题是一个子问题,显然只有同构的子树才可以对应&#xf…

哈希算法在判定树同构方面的应用(下)

哈希算法在判定树同构方面的应用 在上一篇文章中我们介绍了 枚举根节点哈希 和 求重心哈希 两种方法来判断两棵无根树是否同构。 但是如果有些题目中我必须要计算出每个根节点的 f f f 值,且 n ≤ 1 e 5 n\le 1e5 n≤1e5,我们要怎么办呢?…

树的同构判断

使用递归的思路解决树的同构的判断 给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称两棵树是“同构”的。例如图1给出的两棵树就是同构的,因为我们把其中一棵树的结点A、B、G的左右孩子互换后,就得到另外一棵树。而图2…

树同构的判断

结构中的flag用来标记该元素是否被访问过&#xff0c;judge函数中的flag为了使一行的四个元素即使又被访问过的元素也要读完一整行。 #include<stdio.h> #include<stdbool.h> #include<stdlib.h> typedef struct TreeNode *Tree; struct TreeNode{int v;Tre…

哈希算法在判定树同构方面的应用(上)

哈希算法在判定树同构方面的应用&#xff08;上&#xff09; &#xff08;一&#xff09;需要掌握的前置知识&#xff1a; &#xff08;1&#xff09;素数筛法&#xff1a;埃氏筛或者欧拉筛均可以。 以下为欧拉筛&#xff1a; const int maxn100100; int p[maxn],cnt0; bool…

树同构判定算法

树同构判定 树同构判定 图同构与树同构 同的同构问题还没有有效算法。 树的同构本质上寻找不同树之间的双射关系。 通过对树编码&#xff0c;将树的同构问题转化为编码比较问题。 有根树的同构严格强于图同构关系。 如上&#xff0c;图同构的两张图转化成树&#xff0c;…