I/O Completion Port

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

Java代码 
  1. Registers:  2-5 nano seconds  
  2. CPU Cache: 3-10 nano seconds  
  3. RAM: 80-400 nano seconds  
  4. 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数据,另一边是一个小小的线程池,这个 portio数据交给线程池里的线程来处理。同时,别的线程启动了 IO异步请求后通知这个 port一声,“嘿,注意了,一会儿这个 IO handle 会有个数据包传过来要处理。” 这个 port回答,“好,我注意一下这个handle。”。 

 

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

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

Java代码 
  1. HANDLE CreateIoCompletionPort(  
  2.   HANDLE hfile,  
  3.   HANDLE hExistingCompPort,  
  4.   ULONG_PTR CompKey,  
  5.   DWORD dwNumberOfConcurrentThreads);  
  6.   
  7. // 创建IoCompletionPort  
  8.   
  9. HANDLE hCp;  
  10. hCp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 04);  


创建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 这个函数。 

Java代码 
  1. 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  PortCompletion Queue里面加个Entry。这个Entry包括下列数据。 

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


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

 

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

Java代码 
  1. 1.Inactive threads waits IO Completion Port  
  2. 2.Active running threads  
  3. 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。 

Java代码 
  1. BOOL GetQueuedCompletionStatus(  
  2.   HANDLE       hCompPort,  
  3.   PDWORD       pdwNumBytes,  
  4.   PULONG_PTR   CompKey,  
  5.   OVERLAPPED** ppOverlapped,  
  6.   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的系统上,自己实现一个,也未必不可。当然讨论讨论是如何实现的,也是件很有趣的事情。这方面我也没有完全搞个水落石出。让我先攒攒劲,改天再写吧。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值