最近读了部分libevent的源码,梳理并记录一下以加深自己的理解,也方便自己以及有需要的人后续查阅。
在服务器编程中,I/O事件、信号和定时事件是服务器程序基本都要处理和考虑的三类事件。统一处理这三类事件能使代码逻辑清晰,简单易懂。比较常用的处理方法是利用I/O复用系统。但是在不同的系统有不同的I/O复用方式,如Solaris的dev/poll文件,FreeBSD、Mac的kqueue机制,Linux的epoll等。因此,对可能运行在不同系统的服务器程序来说,如何跨平台是个必须考虑的问题。此外,我们还需要考虑各执行实体如何协同处理客户连接、信号和定时器等,避免出现竞态条件。
Libevent就是开源社区提供的一个解决了上述问题的轻量级的I/O框架库,一些常用的开源软件如memcache,nodejs似乎都使用了libevent,类似的框架库还有ACE、ASIO等。
常用的框架库采用的模式有Reactor模式和Proactor模式。同步I/O模型通常用于实现Reactor模式,异步I/O模型通常用于实现Proactor模式。大多数I/O框架库都是以这两种模式中的一种或两种来实现的。Libevent采用的是Reactor模式。接下来先看一下什么是Reactor模式。
Reactor模式通常要求主线程(对单线程来说可能是I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即将该事件通知工作线程(对单线程来说是可能是逻辑单元)。除此之外,主线程(或I/O处理单元)不做其它任何实质性的工作。读写数据,接收新连接以及处理客户请求均在工作线程(或逻辑单元)中完成。一个epoll实现的Reactor模式的工作流程可能如下图2-1所示。
图2-1 Reactor工作流程
图2-1主要包括以下流程:
1) 主线程往epoll内核事件表中注册socket上的读事件
2) 主线程调用epoll_wait等待socket上有数据可读
3) socket有数据可读,epoll_wait通知主线程,主线程将socket可读事件放入请求队列。
4) 睡眠在请求队列上的某个工作线程被唤醒,从socket读取数据,处理请求,如果需要应答,注册该socekt的写就绪事件
5) 主线程调用epoll_wait等待socket可写
6) 当socket可写时,epoll_wait通知主线程。主线程将可写事件放入请求队列
7) 工作线程被唤醒,往socket上写入要返回给客户端的数据。
Reactor模式通常需要包括以下部分:事件循环框架Reactor,事件处理器EventHandler,句柄(也叫事件源)Handler,事件多路分发器EventDemultiplexer和具体的事件处理器ConcreteEventHandler。这些组件的关系如下图2-2所示。
图2-2 I/O框架库组件
接下来对每个组件做逐一说明:
a) 事件源或句柄(Handle)
在Linux上是指文件描述符或信号,在windows上是指Socket或者Handle。程序在指定的句柄上注册关心的事件,比如I/O读写事件,信号事件,超时事件等。
b) 事件处理器(EventHandle)和具体事件处理器(ConcreteEventHandle)
事件处理器通常是一个接口,有的实现包括handle_event回调函数,有的包括handle_input, handle_output, handle_timeout三个具体事件回调函数。用户需要根据自己的业务逻辑继承它来实现自己的具体事件处理器。事件处理器通常还应该包括一个获取句柄的方法,返回当前事件处理器关联的是哪个句柄。
c) 事件多路分发器(EventDemultiplexer)
当前Reactor使用的I/O多路复用机制,如select、epoll等。一般包括事件注册、移除和事件分发三个方法。分别用以向I/O复用注册,取消注册,以及调用select、poll、epoll_wait等等待函数。
d) Reactor反应器
事件管理的接口,内部调用EventDemultiplexer的注册注销接口进行注册、注销事件;运行事件循环。
综上,Reactor的一个运行时序图如下图2-3所示。
图2-3 Reactor模式工作时序图
注:本笔记基于libevent-2.0.21版本,阅读源码过程中一些不懂的地方参考了博客 http://blog.csdn.net/sparkliang/article/category/660506 和《Linux高性能服务器编程》一书