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

这是对[url=http://www.iteye.com/topic/102007]http://www.iteye.com/topic/102007[/url]的一个回帖,颇受好评,得了五星,加了若干分,故收入博客。

基本没什么大错误,经iunknown指正,对non-blocking IO的理解有误。等闲一点,好好翻翻书再改正吧。

--------------------------------

我来侃一侃我对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如下:

[code]
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)
[/code]

如今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。”。

[img]http://bigpanda.iteye.com/upload/picture/pic/4395/2093ef82-a3c7-4a44-a4ec-82b42904b547.jpg[/img]

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

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

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

// 创建IoCompletionPort

HANDLE hCp;
hCp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 4);
[/code]

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

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

[img]http://bigpanda.iteye.com/upload/picture/pic/4396/93d7105d-bcf1-4b89-965c-020228f93c41.jpg[/img]

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

[code]
CreateIoCompletionPort(myHandle, hCp, myKey, 0);
[/code]

第一个参数是个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包括下列数据。

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

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

[img]http://bigpanda.iteye.com/upload/picture/pic/4397/23c52208-b701-45a8-aa4c-4bb091da494b.jpg[/img]

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

[code]
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).
[/code]

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

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

第一个参数是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太不好用了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值