首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

【Windows核心编程学习札记】I/O完成端口(I/O Completion Port)

2012-11-26 
【Windows核心编程学习笔记】I/O完成端口(I/O Completion Port)一、异步设备I/O基础与计算机执行的大多数其他

【Windows核心编程学习笔记】I/O完成端口(I/O Completion Port)

一、异步设备I/O基础


与计算机执行的大多数其他操作相比,设备I/O是其中最慢、最不可预测的操作之一。但是,使用异步I/O能够更好的使用资源并创建出更加高效的应用程序。

假设一个线程向设备发出一个异步I/O请求。这个I/O请求被传给设备驱动程序,后者负责完成实际的I/O操作。当驱动程序在等待设备响应的时候,应用程序的线程并没有因为要等待I/O请求完成而被挂起,线程会继续运行并执行其他有用的任务。

到了某一时刻,设备驱动程序完成了队列中的I/O请求,这时它必须通知应用程序数据已发送,数据已收到或者是发生了错误。

把异步I/O请求加入队列时设计高性能、可伸缩性好的应用程序的本质所在。为了以异步的方式来访问设备,必须先调用CreateFile,并在dwFlagsAndAttributes参数中指定FILE_FLAG_OVERLAPPED标志来打开设备。该标志告诉系统要以异步的方式来访问设备。

为了将I/O请求加入设备驱动程序的队列中,必须使用ReadFile和WriteFile函数:

技术

摘要

触发设备内核对象

当向一个设备同时发出多个I/O请求的时候,这种方法没什么用。它允许一个线程发出I/O请求,另一个线程对结果进行处理。

触发事件内核对象

这种方法允许我们向一个设备同时发出多个I/O请求。它允许一个线程发出I/O请求,另一个线程对结果进行处理。

使用可提醒I/O                  

这种方法允许我们向一个设备同时发出多个I/O请求。发出I/O请求的线程必须对结果进行处理。

使用I/O 完成端口

这种方法允许我们向一个设备同时发出多个I/O请求。它允许一个线程发出I/O请求,另一个线程对结果进行处理。这项技术具有高度的伸缩性和最佳的灵活性。
触发设备内核对象

在Windows中,设备内核对象可以用来进行线程同步,因此对象既可能处于触发状态,也可能处于未触发状态。ReadFile和WriteFile函数在将I/O请求添加到队列之前,会先将设备内核对象设为未触发状态。当设备驱动程序完成了请求之后,驱动程序会讲设备内核对象设为触发状态。
线程可以通过调用WaitForSingleObject或WaitForMultipleObjects来检查一个异步I/O请求是否已经完成。

实例代码:

DWORD SleepEx(         DWORD dwMilliseconds,         BOOL bAlertable);     DWORD WaitForSingleObjectEx(         HANDLE hObject,         DWORD dwMilliseconds,         BOOL bAlertable);     DWORD WaitForMultipleObjectsEx(         DWORD cObjects,         CONST HANDLE* phObjects,         BOOL bWaitAll,         DWORD dwMilliseconds,         BOOL bAlertable);     BOOL SignalObjectAndWait(         HANDLE hObjectToSignal,         HANDLE hObjectToWaitOn,         DWORD dwMilliseconds,         BOOL bAlertable);     BOOL GetQueuedCompletionStatusEx(         HANDLE hCompPort,         LPOVERLAPPED_ENTRY pCompPortEntries,         ULONG ulCount,         PULONG pulNumEntriesRemoved,         DWORD dwMilliseconds,         BOOL bAlertable);     DWORD MsgWaitForMultipleObjectsEx(         DWORD nCount,         CONST HANDLE* pHandles,         DWORD dwMilliseconds,         DWORD dwWakeMask,         DWORD dwFlags); 
调用上面6个函数之一并将线程置为可提醒状态时,系统会首先检查线程的APC队列。如果队列中至少有一项,那么系统不会让线程进入睡眠状态。系统会将APC队列中的那一项去除,让线程调用回调函数,并传入数据。当回调函数返回时,系统会检查APC队列中是否还有其他的项,如果还有,会继续处理。如果没有,对可提醒函数的调用会返回。(调用这些函数的时候APC队列中至少有一项,线程就不会进入睡眠状态。)
有两个糟糕的问题:回调函数和线程问题。回调函数会使得代码实现变得更加复杂,由于这些回调函数一般来说并没有足够的与某个问题有关的上下文信息,因此最终不得不将大量的信息放在全局变量中。线程问题是大问题:发出I/O请求的线程必须同时对完成通知进行处理。如果一个线程发出多个请求,那么即使其他线程完全处于空闲状态,该线程也必须对每个请求的完成通知做出响应。由于不存在负载均衡机制,因此应用程序的伸缩性不会太好。
QueueUserAPC允许我们手动地将一项添加到APC队列中。可以使用其进行非常高效的线程间通信,甚至能跨越进程的界限。但遗憾的是,我们只能传递一个值。也可以用来强制让线程退出等待状态(干净退出)。



三、I/O完成端口

在历史上,架构一个服务应用程序的模型有两种:

(1)串行模型。一个线程等待一个客户发出请求,当请求到达的时候被唤醒并对客户请求进行处理。

(2)并发模型。一个线程等待一个客户请求,并创建一个新的线程来处理请求。当新线程正在处理客户请求的时候,原来的线程会进入下一次循环并等待另一个客户的请求。当处理客户请求的线程完成整个处理过程的时候,该线程就会终止。


Windows中便使用了并发模型的服务应用程序,但是发现性能并不如预期的高。开发组意识到同时处理多个客户请求意味着系统中有许多线程并发执行,由于所有这些线程都处于可运行状态,因此Windows内核在各个可运行的线程之间进行上下文切换花费了太多时间,以至于各个线程都没有多少CPU时间来完成他们的任务了。为了解决这个问题,便出现了I/O完成端口。


1、创建I/O完成端口

I/O完成端口背后的理论是并发运行的线程的数量必须有一个上限。可运行的线程的数量一般约等于CPU的数量,一旦可运行的线程数量大于可用的CPU的数量,系统必须花时间来执行线程的上下文切换,这会浪费宝贵的CPU时间---这也是并发模型的一个潜在缺点

并发模型的另一个缺点是需要为每个客户请求创建一个新的线程。虽然和重建一个新的进程相比,开销小得多,但是仍然不能算小。如果能在应用程序初始化的时候创建一个线程池,并让线程池中的线程在程序运行期间一直保持可用状态,那么服务应用程序的性能就能得到提高。I/O完成端口的设计初衷就是雨线程池配合使用。

创建一个I/O完成端口的函数是:

BOOL GetQueuedCompletionStatus(   HANDLE       hCompPort,   PDWORD       pdwNumBytes,   PULONG_PTR   CompKey,   OVERLAPPED** ppOverlapped,   DWORD        dwMilliseconds);


 第三个与I/O完成端口关联的数据结构是线程等待队列。

线程池内每个调用GetQueuedCompletionStatus的线程的ID被放到正在线程等待队列中,以使I/O完成端口内核对象能够知道当前哪些线程正在等待处理完成的I/O请求。当在该完成端口的I/O完成队列中出现新的项时,完成端口从正在线程等待队列中挑出一个线程唤醒。
正如所期望的那样,I/O完成队列中的项是以先进先出(FIFO)的方式删除的。但是,出乎意料的是,调用GetQueuedCompletionStatus的线程却是以后进先出(LIFO)的方式被唤醒。这么做的原因是为了提高性能。比如说,在线程等待队列中有四个线程,当已完成的I/O项出现时,最后一个调用GetQueuedCompletionStatus的线程将被唤醒来处理该项。这个最后的线程在处理完成后,又调用GetQueuedCompletionStatus重新进入线程等待队列。现在如果又出现了一个I/O完成项,同一线程又会被唤醒来处理新项。
 
当I/O请求的完成慢到单个线程都能够处理时,系统将一直唤醒同一线程进行处理,其他三个线程持续休眠。通过使用LIFO算法,没有被调度的线程可以将它们的内存资源(如堆栈空间)对换到磁盘并从进程的缓冲区内清空。这意味着即使众多线程在完成端口上等待也并非坏事。如果有几个线程在等待,但只有很少的I/O请求完成,多余的线程一定会将它们的大部分资源对换出系统。 


热点排行