Epoll与异步非阻塞

来源:互联网 发布:linux打包文件夹 编辑:程序博客网 时间:2024/06/05 09:37

    走出大学印象最深刻的就是异步的编程模型,还记得第一次见到奇怪的Epoll时的惊喜和兴奋,然后读了Tornado、Nginx、Redis等源码之后恍然大悟。本文主要以Tornado中的实现说明,所以主要使用Python代码,会加Redis的实现做对比,所以也会有部分的C代码,后期会加上Nginx的实现做对比。

  • 异步和非阻塞的区别

    首先说明两者不是一个层次的东西,非阻塞是对于socket而言,而异步是相对于应用程序而言,是一种编程模型,网上的很多文章对此解释不清,甚至混为一谈。

    Epoll是非阻塞的,但不是异步的。实现非阻塞很简单,只要一句socket.setblocking(False)就够了。实现异步却很复杂,在《UNIX网络编程》中给出的5中I/O模型,只有最后一种是异步模型,其他的都是非异步的。

    Linux没有实现异步IO(效率并不高),Epoll是一种I/O多路复用技术,用户程序需要主动的去询问内核是否有事件发生,而不是事件发生时内核主动的去调用回调函数,所以不是异步的。而Tornado等框架之所以声称是异步的,是框架在epoll的基础上进行了一层封装,由框架去取事件,然后由框架去调用用户的回调函数,所以对于基于框架的用户程序来说,是异步的。

    阻塞模式下,内核收到I/O事件会唤醒处理者,而在非阻塞模式下,会将I/O事件放到其他对象(select,epoll)中甚至忽略。

      Tornado使用Epoll实现了异步的编程模型,使用异步的前提是socket是非阻塞的,

  • Epoll的使用

    Epoll的使用只需要三个函数:

         1、int epoll_create(int size);创建一个epoll句柄。内核会建立一系列的数据结构,比如就绪事件队列ready_list_link,具体数据结构下次再分析。

         2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);注册事件,将fd添加(或删除或修改)到刚创建的几个epoll数据结构中。

         3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);查询是否有事件发生,这里一般是各种框架死循环的地方。

  • Tornado对Epoll的封装

    以Tornado的实现为例,对Epoll的封装是在ioloop模块中的IOLoop类。首先是一个单例模式,函数instance,用于获取全局唯一的epoll。最主要的是一个死循环,在start函数中,一直调用

            try:                event_pairs = self._impl.poll(poll_timeout)            except Exception, e:
也就是前面提到的epoll_wait函数,当event_pairs为空时,会阻塞在这里,当event_pairs不为空时,则去执行相应的handler,执行完再回来调用这个函数,整个进程会一直在这个函数中死循环。

    Tornado是一个单线程框架,它不会像多线程那样,一个线程阻塞了,操作系统会调度另一个线程,如果一处阻塞,就会导致整个进程阻塞,这就要求任何代码都必须是非阻塞的,所以Tornado的iostream模块的IOStream类实现了对非阻塞的socket的封装(IOStream实现的功能远不只是非阻塞,它在socket之上加了一个缓存层,并且有一些方便HTTP协议调用的函数),如下

class IOStream(object):    r"""A utility class to write to and read from a non-blocking socket.    """    def __init__(self, socket, io_loop=None, max_buffer_size=104857600,                 read_chunk_size=4096):        self.socket = socket        self.socket.setblocking(False)        self.io_loop = io_loop or ioloop.IOLoop.instance()
可以看到self.socket.setblocking(False)将socket设置为了非阻塞,而self.io_loop则是单例模式的全局ioloop对象。对ioloop的操作在_add_io_state函数中

def _add_io_state(self, state):        """Adds `state` (IOLoop.{READ,WRITE} flags) to our event handler.        """        if self.socket is None:            # connection has been closed, so there can be no future events            return        if self._state is None:            self._state = ioloop.IOLoop.ERROR | state            with stack_context.NullContext():                self.io_loop.add_handler(                    self.socket.fileno(), self._handle_events, self._state)        elif not self._state & state:            self._state = self._state | state            self.io_loop.update_handler(self.socket.fileno(), self._state)
可以看到调用了self.io_loop.add_handler函数和self.io_loop.update_handler函数,这两个函数都是对应上面提到的epoll_ctl函数,即添加、更新事件监听。

  • Tornado的异步方式
    Tornado有两个类CurlAsyncHTTPClient和SimpleAsyncHTTPClient来实现异步的client,这里说明SimpleAsyncHTTPClient,它是对_HTTPConnection类的一层封装,主要是fetch函数提供给用户调用
def fetch(self, request, callback, **kwargs):
对_HTTPConnection的调用是在_process_queue函数
def _process_queue(self):        with stack_context.NullContext():            while self.queue and len(self.active) < self.max_clients:                request, callback = self.queue.popleft()                key = object()                self.active[key] = (request, callback)                _HTTPConnection(self.io_loop, self, request,                                functools.partial(self._release_fetch, key),                                callback,                                self.max_buffer_size)

_HTTPConnection类是对上面提到的IOStream类的一层封装,SimpleAsyncHTTPClient会将url和回调函数callback传入_HTTPConnection,而_HTTPConnection会调用IOStream通过IOLoop将事件和callback注册入Epoll中。

    对于异步,多个回调函数时是很复杂的,回调函数中又包含回调函数,Tornado提供了gen模块,它利用yield语法,可以以同步的编程方式编写异步的代码。具体下次再分析。

  • 优点
        1、对于多进程多线程模型来说,Tornado的这种异步事件驱动模型减少了上下文的切换,减少了从内核态到用户态的切换,IOLoop中的死循环起到了操作系统调度的作用,只是它不按照时间片规则。

        2、Epoll对于select,select是内核对数组置位的方式标识某个fd有事件发生,每次都要线性的遍历整个数组去判断是否有事件发生;而Epoll是内核将就绪事件用链表链接起来,每次只需要访问这个就绪链表,而不用访问未就绪的fd。Epoll相对于select的优势是去掉了轮询。

        3、Epoll没有对fd个数的限制,而select有限制。

  • 缺点
        1、Tornado是单线程,所以所有的I/O操作必须是非阻塞的,否则会阻塞住整个进程,这就意味着很多的库不能使用,比如urllib、httplib,而必须使用IOStream或自己封装IOLoop。Tornado提供了一些异步的lib,在https://github.com/facebook/tornado/wiki/Links,包括常用的MongDB、Memcached等,可以弥补一些这方面的不足。

        2、编程复杂,很多的回调函数是逻辑不清晰,也不利于调试,Tornado提供的gen模块改善了一下。





0 0
原创粉丝点击