深入了解IO完成端口

来源:互联网 发布:江苏三六五网络招聘 编辑:程序博客网 时间:2024/04/30 09:10

深入了解I/O完成端口

介绍:

编写一个高性能的服务器需要实现一个高效的线程模型。过多或过少的用来处理客户请求的服务器线程都有可能导致性能问题。比如,如果服务器只创建一个线程来处理所有客户端请求,则会导致“饿死”的事情发生,因为在一段时间内,服务器资源被绑定到一个客户请求了。虽然一个单一的线程也可以同时处理多个请求,从一个I/O操作转移到另外一个I/O操作,但是这种结构导致了很大的复杂度,并且不能发挥多CPU的优势。另一个极端是,服务器可以创建一个非常大的线程池以便模拟每个客户端请求在服务器上都有一个专用线程来处理。这种情况,经常会导致线程震动,一些线程醒来,执行一些CPU操作,由于I/O操作被阻塞,等处理完一个请求后,再次阻塞等待新的请求。并且,上下文切换将会引起调度程序不得不把处理器时间在大量活动线程中切分。

高性能服务器的设计目标是尽可能的减少线程阻塞,以便减少上下文切换,同时使用多线程技术,尽可能的提高线程的并行处理时间。 这个目标的要点是,在每个处理器上都必须有一个活动的线程来处理客户端请求,并且如果每个线程处理完当前请求后,只要请求队列上还有请求,这个线程就不能进入等待状态。 这个想法要能正常工作,必须有一种方法,当一个线程由由IO操作被阻塞时,应用程序能够激活另外一个线程。

Windows NT3.5引入了一系列API,这些API能够让你相对容易的达到这个目标。这些API主要围绕一个称为完成端口的对象,在这篇文章中,我将给你介绍完成端口的一些细节,并带领你 深入了解,WindowsNT是怎么实现的。

使用IO完成端口:

应用程序使用完成端口的焦点在于,完成端口能够关联多个文件句柄。一旦一个文件被关联到完成端口,任何异步IO操作完成,都会有一个完成消息包被排队到这个端口。 一个线程只要简单的等待被排队到完成端口上的完成消息包,就可处理多个文件中的标准IO完成事件。Win32API WaitForMultipleObjects提供相似的功能,但完成端口的优势在于并发性,并且用于服务客户端请求的 线程数目可以在操作系统的帮助下得到控制。

当应用程序创建一个完成端口时,它需要指定一个并发值。这个值意味着在任何时间,跟这个完成端口关联的可运行的线程的最大数目。就像我当初所说的,这个方法的要点是在任何时候,系统的每个处理器上都必须有一个活动的线程。NT用这个并发值来控制应用程序有多少个线程可以激活,如果跟完成端口关联的活动线程数目等于这个并发值,则另外一个正在该完成端口等待的线程将不会被允许执行。同理对于正在处理的活动线程来说,当他处理完当前的包,需要去检查是否还有其他包在等待,如果有,他只是简单的把这个新报拿出来,继续处理。当这种情形发生时,是没有上下文切换的,CPU的利用率最好。

下图描述的是完成端口操作的一个高层模型:
客户端有请求发生时,一个完成包会被排队到该完成端口,上限到并发数目的线程将会被NT用来并发处理客户端请求,其他额外的跟该完成端口关联的线程将会被阻塞,直到活动线程的数目减少,这种情形主要 发生在活动线程由于IO操作而被阻塞,我将会在下面详细讨论这个情况。

调用Win 32 API CreateIoCompletionPort创建完成端口

HANDLE CreateIoCompletionPort(
HANDLEFileHandle,
HANDLEExistingCompletionPort,
DWORDCompletionKey,
DWORDNumberOfConcurrentThreads
);
创建一个完成端口时,参数NumberOfConcurrentThreads用来指定并发线程的数目,如果FileHandle 被指定,则FileHandle 将和该完成端口关联到一起。 当这个文件句柄上的IO完成时,一个完成包将会被排队到这个完成端口,调用GetQueuedCompletionStatus 可以检索到完成包或等待完成包的到来, BOOL GetQueuedCompletionStatus(
HANDLECompletionPort,
LPDWORDlpNumberOfBytesTransferred,
LPDWORDCompletionKey,
LPOVERLAPPED*lpOverlapped,
DWORDdwMiillisecondTimeout
);
等待在完成端口上的线程被叫醒的策略为LIFO,即最后等待的将会得到下一个完成包。线程等待的时间过长,它的堆栈将会被交换到硬盘上。

服务端程序一般通过代表网络端点的文件句柄获得客户端请求,比如win socket或命名管道。服务器创建网络端点的时候,关联了完成端口,则跟完成端口关联的 服务线程将会调用GetQueuedCompletionStatus 以求获得客户端的请求包。当一个线程从完成端口获得一个请求包后,它将变成一个活动线程,并开始处理这个包。许多时候, 线程在处理的过程中会被阻塞,比如需要从disk读写数据或同步其他线程。Windows NT是一个优秀的系统,可以检查到这种情况,当线程被阻塞后,完成端口会认为活动线程被减少, 这时如果有包到来,等待的线程将会被激活。

微软的参考手册认为,并发数的设置数为系统中的处理器的数目。注意,完成端口的活动线程的数目是有可能超过并发限制的。考虑下面一种情况,并发数设置为1,客户端请求到了,一个线程被激活,用来处理这个请求。第二个请求到了, 但是等在完成端口上的线程不被允许处理这个包,因为并发数达到了上限。等到第一个线程由于IO操作变成非激活状态,第二个线程才被激活,在第二个线程仍旧处在激活状态时,第一个线程的IO结束了,也变成了激活状态。在这个时刻,除非有线程被阻止了, 否则活动线程数一直是2,大于并发数1的。大部分时间,活动线程的数目等于或刚好大于并发数。

完成端口的API PostQueuedCompletionStatus可以用来投递一个自定义的完成包,服务程序一般用这个函数投递一个关闭包给自己的线程,以便关闭程序。

更多文章,请参考:http://www.thirddata.com/documents.aspx

原创粉丝点击