Linux async io 1

asynchronous io的使用越来越普及。现在有相当多的类库,web服务器和网络编程框架使用了这一技术。比如python的tornado,twisted, c写的httperf, libevent, boost asyncIO。我所在的项目有一个c++开源库http://code.mozy.com/projects/mordor, 也用到了这一技术。顺便提一下,这是一个很强大的库,实现了很多很cool的特性(如fiber).

这个系列的文章计划分为4部分:

1,async io的基础,事件循环

2.  具体io回调处理的实现

3.如何用async io 实现一个socket stream

4.有时间的话,基于async io实现一个http server.

 

先从基础入手,解释一下什么是async io. 普通的blocking io操作,比如说接受socket消息的时候,线程会block住直到io操作完成。

这段时间里cpu实际上处于空闲状态,这造成了很大的浪费。其实,我们完全可以让cpu在系统做io操作的时候同时做其他事情。通常解决这一问题的做法是给每个独立的IO起一个线程。这样虽然解决了CPU过度空闲的问题,但是带来了多线程的额外负担。为了充分利用cpu而又不产生多线程的开销,这就需要async io出场了。
async io的主要思路是让nonblocking IO操作异步的执行从而不block住当前线程。把所有的io操作聚集在一个独立的线程中执行。这个线程是一个事件循环(event loop).它利用select/poll/epoll,在循环里监听注册的文件描述符的新事件。对于每一个io事件,执行之前注册的回调函数。
简单的时序:
thread1: io_manager.start()

thread2: do_something()    ...   doIO() ...  do_something_else()
thread3: do_something2()    ...   doIO() ...  do_something_else2()

事件循环是async io的核心所在。 这篇文章接下来的内容将集中在这一机制的实现上。

理解时间循环,首先要理解linux下的select/poll/epoll. 可以选择其中的任何一个作为async io的底层调用。下文一律将以epoll为例。提一下epoll有一个特别的地方,就是不支持不会block的fd操作, 比如说读写regular file. 对regular fd的epoll_ctl操作将返回 EPERM(The target file fd does not support epoll). 关于这三者的介绍和比较,可google之.

fd的事件概括起来只有如下几种:read, write, close, error.
在循环中,调用epoll_wait()我们可以知道自从上次调用epoll_wait(),有多少fd有新的事件发生,从而得到一个events列表。可以根据event的fd和event_type做相应的业务逻辑操作。伪代码如下:

 

while (1) {
    
// wait for something to do...
    int nfds = epoll_wait(epfd, events,
                                MAX_EPOLL_EVENTS_PER_RUN,
                                EPOLL_RUN_TIMEOUT);
    
if (nfds < 0) die("Error in epoll_wait!");

    
// for each ready socket
    for(int i = 0; i < nfds; i++) {
      
int fd = events[i].data.fd;
      handle_io_on_socket(fd);
    }
  }

 注意这里的handle_io_on_socket(fd), 这只是一个假设的函数。

实际实现的时候,一般的流程是这样的:

1. 启动 io manager线程, io manager一般作为一个sungleton存在。多余的io manager 基本是没有什么意义的。

2. 在业务逻辑线程中,需要读写比如socket时,实现一个asyncRW方法,它调用io manager的registerFD方法,往io manager里注册该socket的fd, event(基本就是read/write)和callback(对应实际的业务逻辑,比如读写fd,然后再做处理). 我将用一个类叫做AsyncIOFD来对这些参数做封装。io manager 里维护一个fd->AsyncIOFD的map.

3. io manager的时间循环中,对每个wait到的fd, 查map得到 AsyncIOFD, 然后调用callback。

4. 当对某一fd的处理完毕,close fd,这将取消epoll的监听。也可以显式地调用io manager的 unregisterFD方法

 

 解释至此,整个流程还是很清晰的。不过我们需要回答一个问题:为什么epoll_wait()会放回事件列表,或者说,events列表不会自动产生,整个逻辑中还缺了一环。

让我们以socket为例来解释这个问题。

首先,socket的read ready事件是比较好理解的。read ready的含义是:有数据需要读,这些数据存在于操作系统底层的socket缓存中.我们和远端的socket建立连接,当远端socket send的数据到达,就会触发本地socket的read ready事件。

再来看看write ready. write需要己方主动触发。不可能自己什么都不做却收到一个write ready事件。write ready的含义是:还有数据需要写,或者说,有数据没写完。而这些数据来自于你的程序之中。 这也是non-blocking write的含义。对于一个non-blocking socket, 调用write立刻返回,这并不确保数据都被写入。保证这一点需要你的程序的控制(比如说检查write的返回是写了多少byte,然后计算是否已经写完了所有的数据)。 而一旦数据没写完,就会有一个write ready事件被触发。 所以,在我们的asyncRW函数中,对于write,我们需要有额外的操作。那就是先write一次,然后再把一切交给io manager线程。否则,我们永远接受不到write ready 的通知!

 

 很多情况下,io manager也是一个定时器 manager. 具体的例子,可以参见httperf(http://www.hpl.hp.com/research/linux/httperf/)的源码。httperf是一个开源的用c实现的性能测试工具。httperf在上文提到的while循环中额外干了一件事情,就是检查定时器,做一些计算和统计工作,然后再调用epoll_wait(或者是select 和pol里l相应的函数)。

 

 事件循环的内容基本就是这些了。后续的内容中会贴上简单的实现代码。

 

posted on 2011-06-05 21:36  freestyleking  阅读(985)  评论(0编辑  收藏  举报

导航