论坛首页 编程语言技术论坛

[Windows]I/O completion ports如何实现?个人推测,寻求标准答案!

浏览 19052 次
精华帖 (4) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-07-18  
...那是上层,driver层当然还是要用到这些,硬件这些又不会变的
0 请登录后投票
   发表时间:2007-07-18  
从那本Windows Internals里面看来的,不知道理解上面有没有偏差。


好像是说OS kernel call到driver然后按照一个flag(同步IO还是异步IO)决定是否立即返回。driver在这个IO完成的时候会调用一个IoCompleteRequest来通知I/O管理器已经完成了这个IRP(I/O request packet),至于驱动怎么知道完成了,如果可能的话多半就是靠中断了。这个时候IO的结果还在内核的I/O status block里面,需要经过一个内核模式的异步过程调用(APC)来copy到用户地址空间。这个过程不需要额外线程,因为内核模式APC可以在用户线程里面执行一段他的code. 然后再APC完成的时候设定那个handle的event状态。如果你正在WaitForSingleObjectEx那么就会被叫醒(如果是同步IO业就是这个时候被叫醒)。

至于ReadFileEx这种可以带一个callback的函数,它会在IO完成的最后一步schedule一个用户状态的APC,但是用户状态的APC需要你在SleepEx或者WaitForSingleObject什么的时候才可以callback给你。

然后,如果你有对于这个文件handle create IOCP那么I/O Manager会在IO完成的时候调用KeInsertQueue把它排队到这个queue里面,叫做一个Completion Package,这个时候如果一个线程调用了GetQueuedCompletionStatus的话他就会把这个IO Completion Package给你让你处理。如果你一个劲地处理,完了以后又回来call GetQueuedCompletionStatus, 系统发现还有IO包,它就继续给你而不需要线程切换。号称这样会有比较高的效率。如果有多个线程等待,而IO包却不那么多,那么线程会以LILO的方式被调度,因为经常处理IO的线程会比较“热”,也就是比较容易cache命中,也不太容易被swap到pagefile里面。

再说说那个个最大并发值(CreateIoCompletionPort的那个NumberOfConcurrentThreads)。这个值是说最多有多少个活动线程。但这个并不是最多同时运行的线程数。假设你把它设成1,那么你的第一个线程拿到了一个Compelion Package开始处理,但如果你的线程在处理这个IO Completion Package的时候又被block了(比如你处理网络IO的时候去同步读写文件了),那么你这个thread就不算做active的,如果有下一个thread在等这个IOCP的话也会被触发。


另外这里有一篇文章好像和书上的某一段一摸一样(我上面基本也是在背书……),怀疑就是某个作者写的
http://www.microsoft.com/technet/sysinternals/information/iocompletionports.mspx
0 请登录后投票
   发表时间:2007-07-19  
我来侃一侃我对I/O Completion Ports的理解吧。这是一个比较复杂的话题,下面的描述尽量详细,争取把来龙去脉讲清楚,勿嫌啰嗦。

首先讨论一下I/O Completion Ports试图解决什么样的问题。

写一个IO Intensive服务器程序,对每一个客户请求生成一个新的child process/worker thread来处理,每个process/thread使用同步IO,这是最经典古老的解法了。在这之上的改进是prefork 多个process 或者使用线程池。(使用process或thread,原理都差不多,thread的context switch花销要比process switch要小。为了论述简单,下面只讨论线程。)

这种结构的并发性并不高,哪怕你用C++, C甚至汇编来写,效率都不会很高,究其原因,在于两点:

一.同步IO,每个线程大多数时间在等IO request的结束。IO相对于CPU,那是极极慢的。我翻了翻手里的Computer Architecture, A Quantitative Approach第二版,1996年出的,里面对CPU Register, CPU Cache, RAM, Disk,列的access time如下:

Registers:  2-5 nano seconds
CPU Cache: 3-10 nano seconds
RAM: 80-400 nano seconds
Disk: 5 000 000 nano seconds (5 milli seconds)


如今CPU又按照摩尔定律发展了十年后,这个硬盘还是机械式的磁头移来移去读写,尽管如今disk controller都有cache,也在发展,但和CPU相比,差距越来越大。(谁有最新数据可以贴上来。)

二.生成数量大大超过CPU总数的线程。这样做有两个弊端,第一是每个线程要占用内存,Windows底下每个thread自己stack的省缺大小为1M,32位程序下一个用户程序最大能利用的内存也就3G,生成3000个线程,内存就没了。当然有人说64位下面,可以随便浪费,那么,第二个弊端,就无法避免了 ─ 生成大量的线程,CPU必然会花费大量的cpu cycles在线程之间进行切换。如今市场上价格适中的服务器也就2 cpu x 4 core = 8 核而已。生成那么多的线程,CPU在切换线程上花的功夫可能比干正经事还要多。

明白了原因,就可以寻找改进方法。首先,使用异步IO。现在所有主流OS,都提供异步IO(non-blocking IO),连Java这种跨平台的编程环境都在版本1.4里开始支持异步IO了。但是,光有异步IO,这是不够的。论坛里有人发贴子问过,“我的线程发个IO Request,异步IO,直接返回了,然后我的线程干什么?” 异步IO是操作系统提供的机制,我们还需要设计我们程序的结构,使异步IO和线程结合起来,可以充分利用异步IO带来的好处,同时必须控制同时运行线程的数量,减少thread context switch的开销。

IO Completion Port, 是微软针对上述思想,在Windows内核级别,提供的解决方案。

从抽象高度去理解IO Completion Port,可以把它想成一个magic port,一边有一个队列是IO驱动程序处理好的IO数据,另一边是一个小小的线程池,这个port把io数据交给线程池里的线程来处理。同时,别的线程启动了IO异步请求后通知这个port一声,“嘿,注意了,一会儿这个IO handle 会有个数据包传过来要处理。” 这个port回答,“好,我注意一下这个handle。”。



下面我们具体看一下Io Completion Port这个内核对象以及使用。

要创建IoCompletionPort,呼叫Win32函数CreateIoCompletionPort。这个函数一身两用,创建IoCompletionPort也是它,往建好的IoCompletionPort里面加device handle也是它。

HANDLE CreateIoCompletionPort(
  HANDLE hfile,
  HANDLE hExistingCompPort,
  ULONG_PTR CompKey,
  DWORD dwNumberOfConcurrentThreads);

// 创建IoCompletionPort

HANDLE hCp;
hCp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 4);


创建IoCompletionPort头三个参数都是NULL之类的,只有第四个参数,用来配置这个生成的IoCompletionPort所允许同时运行的最大线程数目。

创建好的IoCompletionPort kernel object,拥有两个队列。一个是Device List,包含所有通过这个IoCompletionPort管理的异步IO请求 的Device Handle。另外一个是I/O Completion Queue (FIFO),Device Handle对应的IO驱动程序处理好的IO数据,放在这个队列里。



为了能在Device List这个队列里面加个entry,用户程序将再一次使用CreateIoCompletionPort 这个函数。

CreateIoCompletionPort(myHandle, hCp, myKey, 0);


第一个参数是个IO Handle(Windows不限制handle类型,File, Directory, Serial Port, Parallel port, Mailslot server, Mailslot client, pipe, socket等等都可以),第二个参数是以前创建的IoCompletionPort handle. 这个IO Handle将放到IoCompletionPort handle的Device List那个队列里去。第三个参数是个long整数,是用来identify程序Request Context的。因为现在程序不是一个线程来处理客户Request了,而是不同的线程来处理。在一个线程里按顺序一二三四五来实现程序逻辑的方式是不行了,因此作为程序员你要把逻辑的Context记下来,让不同的线程得到这个Context,根据当前的状态,来执行相关的代码。这个completion key,是找到相映的context的key, index, hash code,pointer, whatever.

第二个队列,IO Completion Queue,是由OS往里面插入entry的。OS在处理好了IO异步请求之后,察看一下这个Device handle是否是放在某个Completion Port里面,如果是,OS就在Completion Port的Completion Queue里面加个Entry。这个Entry包括下列数据。

1.Number of bytes transferred
2.Completion key
3.Pointer to I/O request’s OVERLAPPED structure
4.Error code


下面来看看IO Completion Port是怎么管理线程的。



前面说Completion Port有个线程池,这种说法并不是很贴切。Completion Port本身并不创建线程,而只是掌管三个thread队列:

1.Inactive threads waits IO Completion Port
2.Active running threads
3.Threads paused by other reasons, like waiting for something else (i.e. calls WaitForSingleObject, or even stupid but valid, calls Sleep).


线程由程序创建,然后加入第一个队列(waits on IO Completion Port)。为了加入这个队列线程要呼叫一个函数,GetQueuedCompletionStatus。

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


第一个参数是handle to Completion Port,线程通知OS本线程要加入这个Completion Port的第一个队列。这个函数会block当前线程,使其处于inactive状态。

现在再去看看图二的I/O Completion Queue,OS在一份IO异步请求处理好了后,会在这里插入个entry,Completion Port在收到entry后,看看线程池里面有没有空闲没事做的线程,如果有,不要忘记我们创建这个Completion Port时候规定了个最大同时运行线程数量,如果当前运行线程数量小于这个最大值,那么就把这个线程放到第二个(active running)的队列上去,让这个线程运行起来。前面不是说线程在GetQueuedCompletionStatus上面block了么,现在这个函数返回了,继续运行程序的代码。通过这个最大同时运行线程数量,保证了不会有太多的线程在运行,Viola! 本文开头分析的几个问题全解决了。即是异步IO,又把异步IO和线程池结合了起来,还控制了当前运行线程数量。It’s BEAUTIFUL!

这个线程处理完程序逻辑后,呼叫一下GetQueuedCompletionStatus,又回到了第一个队列。有意思的是这个队列的逻辑是Last In First Out。如果又有IO数据等待线程处理,这个线程可以继续执行,不用进行Context Switch,典型的能者多劳啊,越能干的人干的越多。

这个线程在处理程序逻辑的过程中,可能会因为别的原因而变成inactive,比如在等别的资源(WaitForSingleObject),或者变态一点,自己来了个Sleep,这时线程就给放到第三个队列去了。

这里有个有趣的现象,假如开始我们在第一个队列里面放三个线程,而最大同时运行线程数量设为2,在两个线程跑起来之后,第三个就不跑了,如果这时运行中的某个线程因为等别的资源而变为inactive,那么第三个线程也开始跑起来,同时运行线程数量还是2,这时那个等别的资源的线程等到资源了,又开始跑了起来,这时同时运行线程数量就是3,比设定的2要大。

推荐的最大同时运行线程数量一般为CPU的总数,但是如果运行的线程还要等别的资源,建议把这个数目稍微设大一点,这样并发率会更高。

先写这么多吧,累死了。这个帖子讲述的是IO Completion Port的工作原理,还没有谈到它是如何实现的,也还没有扯到Windows Source Code. 如果对工作原理了解了,也未必要去深究是如何实现的。在没有IO Completion Port的系统上,自己实现一个,也未必不可。当然讨论讨论是如何实现的,也是件很有趣的事情。这方面我也没有完全搞个水落石出。让我先攒攒劲,改天再写吧。

另外搭个顺风车问个问题,我做图的水平很臭,手里就有个Visio还用不好。本文的图都是在Word里面画的。请问能否推荐个好用的作图软件,免费还是花钱的都可以。要有什么画UML比较好的软件,也推荐推荐,Visio太不好用了。
2 请登录后投票
   发表时间:2007-07-19  
看了 bigpanda 的描述,如果 IOCP 真是这样工作的话,那么甚至可以认为这是一个在内核实现的 Proactor 模型。在 Win32 平台,ACE 的 Proactor 是基于 OVERLAPPED 来实现的。不知道 Proactor 和 IOCP 哪个先出现呢?

bigpanda 写道
但是,光有异步IO,这是不够的。论坛里有人发贴子问过,“我的线程发个IO Request,异步IO,直接返回了,然后我的线程干什么?” 异步IO是操作系统提供的机制,我们还需要设计我们程序的结构,使异步IO和线程结合起来,可以充分利用异步IO带来的好处,同时必须控制同时运行线程的数量,减少thread context switch的开销。


这里没有进一步阐述为什么“光有异步IO,这是不够的”。
有了异步 IO 之后,为什么还需要线程呢?
这是因为在某些场景下使用异步 IO 来处理是非常合适的,但是还有些场景下,使用异步 IO 是非常麻烦的,反而是“每一个请求对应一个worker thread,使用同步IO的最经典古老的解法”是合适的。
使用异步 IO 的场景,典型的就是处理 ServerSocket 读写的时候。作为服务器端,请求的接收和响应的发送,使用异步 IO 非常合适。
对应地,考虑在 ClientSocket 的场景下,如果使用异步 IO 就未必合适了。比如常用的 jdbc api ,如果是做成 IOCP 的那种风格,那么将会加大软件的开发难度。曾经看过一篇“Why Threads Are A Bad Idea”,里面提到线程纯粹是为了编程方便而设计的,对于解决 IO 速度和 CPU 速度不匹配的问题根本没有什么作用。从某个角度来说,的确可以这样认为。在 ClientSocket 的场景下,使用异步 IO 不是说不能用,只是难度比起使用线程高了很多。

通常来说,一个典型的服务器程序,将会同时涉及 ServerSocket 和 ClientSocket,那么在这样的场景中,就需要结合异步 IO 和线程池了。

引用
现在所有主流OS,都提供异步IO(non-blocking IO),连Java这种跨平台的编程环境都在版本1.4里开始支持异步IO了。


其实只有类似 IOCP 这样的接口才能说是异步 IO 。虽然很多情况下,异步 IO 是基于 non-blocking IO 封装出来的。当时,仍然不能把异步 IO 和 non-blocking IO 混为一谈,异步 IO 和 non-blocking IO 在接口上是不同的。这一点在 Richard Stevens 的 <<unix network programming>> 一书中有很详细的分析。
1 请登录后投票
   发表时间:2007-07-21  
iunknown 写道
看了 bigpanda 的描述,如果 IOCP 真是这样工作的话,那么甚至可以认为这是一个在内核实现的 Proactor 模型。在 Win32 平台,ACE 的 Proactor 是基于 OVERLAPPED 来实现的。不知道 Proactor 和 IOCP 哪个先出现呢?


IOCP从是NT 3.5的时候出来,时间大概和windows 95差不多.
0 请登录后投票
   发表时间:2007-07-22  
我对windows没什么了解
不过按照公司干这个的人的解释
感觉IOCP就是加入了线程的kqueue
内核会负责收集各种状态改变信息
然后分门别类的放入队列
然后队列的通知注册这个队列消息的进程
FreeBSD里面,当进程觉得得到足够通知的时候,他会让kevent函数返回,这样用户态程序可以去处理这些发生的事情
Windows应该就是在得到通知之后唤醒线程池里面的线程,然后线程去执行已经注册的回调函数
0 请登录后投票
   发表时间:2007-07-22  
我用gnu绘图软件dia
http://www.gnome.org/projects/dia/
有win32版本
0 请登录后投票
   发表时间:2007-07-23  
whisper 写道
我对windows没什么了解
不过按照公司干这个的人的解释
感觉IOCP就是加入了线程的kqueue
内核会负责收集各种状态改变信息
然后分门别类的放入队列
然后队列的通知注册这个队列消息的进程
FreeBSD里面,当进程觉得得到足够通知的时候,他会让kevent函数返回,这样用户态程序可以去处理这些发生的事情
Windows应该就是在得到通知之后唤醒线程池里面的线程,然后线程去执行已经注册的回调函数


进程??线程的开销应该比进程小一些吧。
0 请登录后投票
   发表时间:2007-08-07  
iocp有完整的原代码,他其实是对WIN系统的一个对象的封装。
0 请登录后投票
   发表时间:2007-08-07  
iocp在内核里面是个由KeInitializeQueue产生的队列。
VOID  KeInitializeQueue(    IN PKQUEUE  Queue,    IN ULONG  Count  OPTIONAL    );
这个Count就对应createiocp里的那个参数。
请参看:
http://msdn2.microsoft.com/en-us/library/ms796232.aspx
每个filehandle内核结构都有一个成员,用来存放他关联的iocp,当DeviceIoControl结束的时候,如果发现hevent没设这个iocp又有值,就把这次DeviceIoControl的结果塞到这个队列中去。
我们自己开线程来取这个队列中的完成消息。

其实没什么神秘的,就是一个队列而已。
iocp的函数就是对KQUEUE(WIN)的一个封装。

然后iocp怎么保证每次只有N个线程在运行的呢,其实说白了很简单。
每个线程有个成员叫Queue,当这个线程领到一个任务的时候(GET成功),会把这个地方赋值为这个IOCP的队列,同时kqueue的CurrentCount变量+1,(先前传进去的叫MacCount),他会比较这两个值,假如当前运行的线程大于这个的话,就会阻塞掉了。参见wrk中ke目录中queueobj.c,其实很清晰的。

就想象成一个消息队列,很简单的概念。
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics