libevent源码深度剖析

原文地址: http://blog.csdn.net/sparkliang/article/details/4957667react

 

                                                                       第一章面试

1,前言编程

Libevent是一个轻量级的开源高性能网络库,使用者众多,研究者更甚,相关文章也很多。写这一系列文章的用意在于,一则分享心得;二则对libevent代码和设计思想作系统的、更深层次的分析,写出来,也可供后来者参考。设计模式

附带一句:Libevent是用c语言编写的(MS大牛们都偏心c语言哪),并且几乎是无处不函数指针,学习其源代码也须要至关的c语言基础。数组

 

2,libevent简介缓存

上来固然要先夸奖啦,Libevent 有几个显著的亮点:
  => 事件驱动(event-driven),高性能;
  => 轻量级,专一于网络,不如ACE那么臃肿庞大;
  => 源代码至关精炼、易读;
  => 跨平台,支持Windows、Linux、*BSD和Mac Os;
  => 支持多种I/O多路复用技术, epoll、poll、dev/poll、select和kqueue等;
  => 支持I/O,定时器和信号等事件;
  => 注册事件优先级;
安全

Libevent已经被普遍的应用,做为底层的网络库;好比memcached、Vomit、Nylon、Netchat等等。服务器

Libevent当前的最新稳定版是1.4.13;这也是本文参照的版本。网络

 

3,学习的好处数据结构

学习libevent有助于提高程序设计功力,除了网络程序设计方面外,Libevent的代码里有不少有用的设计技巧和基础数据结构,好比信息隐藏、函数指针、c语言的多态支持、链表和堆等等,都有助于提高自身的程序功力。
程序设计不止要了解框架,不少细节之处偏偏也是事关整个系统成败的关键。只对libevent自己的框架大概了解,那或许仅仅是只知其一;不知其二,不深刻代码分析,就难以了解其设计的精巧之处,也就难觉得本身所用。

事实上Libevent自己就是一个典型的Reactor模型,理解Reactor模式是理解libevent的基石;所以下一节将介绍典型的事件驱动设计模式——Reactor模式。

 

参考资料:Libevent: http://monkey.org/~provos/libevent/

 

                                                                       第二章

前面讲到,整个libevent自己就是一个Reactor,所以本节将专门对Reactor模式进行必要的介绍,并列出libevnet中的几个重要组件和Reactor的对应关系,在后面的章节中可能还会提到本节介绍的基本概念。

1,Reactor的事件处理机制

首先来回想一下普通函数调用的机制:程序调用某函数?函数执行,程序等待?函数将结果和控制权返回给程序?程序继续处理。

Reactor释义“反应堆”,是一种事件驱动机制。和普通函数调用的不一样之处在于:应用程序不是主动的调用某个API完成处理,而是偏偏相反,Reactor逆置了事件处理流程,应用程序须要提供相应的接口并注册到Reactor上,若是相应的时间发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。使用Libevent也是想Libevent框架注册相应的事件和回调函数;当这些时间发声时,Libevent会调用这些回调函数处理相应的事件(I/O读写、定时和信号)。

    用“好莱坞原则”来形容Reactor再合适不过了:不要打电话给咱们,咱们会打电话通知你。

    举个例子:你去应聘某xx公司,面试结束后。

“普通函数调用机制”公司HR比较懒,不会记你的联系方式,那怎么办呢,你只能面试完后本身打电话去问结果;有没有被录取啊,仍是被据了;

“Reactor”公司HR就记下了你的联系方式,结果出来后会主动打电话通知你:有没有被录取啊,仍是被据了;你不用本身打电话去问结果,事实上也不能,你没有HR的留联系方式。

 

2 ,Reactor模式的优势

Reactor模式是编写高性能网络服务器的必备技术之一,它具备以下的优势

    1)响应快,没必要为单个同步时间所阻塞,虽然Reactor自己依然是同步的;
    2)编程相对简单,能够最大程度的避免复杂的多线程及同步问题,而且避免了多线程/进程的切换开销;
    3)可扩展性,能够方便的经过增长Reactor实例个数来充分利用CPU资源;
    4)可复用性,reactor框架自己与具体事件处理逻辑无关,具备很高的复用性;

3 ,Reactor模式框架

 使用Reactor模型,必备的几个组件:事件源、Reactor框架、多路复用机制和事件处理程序,先来看看Reactor模型的总体框架,接下来再对每一个组件作逐一说明。

 

1) 事件源
Linux上是文件描述符,Windows上就是Socket或者Handle了,这里统一称为“句柄集”;程序在指定的句柄上注册关心的事件,好比I/O事件。

2) event demultiplexer——事件多路分发机制
由操做系统提供的I/O多路复用机制,好比select和epoll。
    程序首先将其关心的句柄(事件源)及其事件注册到event demultiplexer上;
当有事件到达时,event demultiplexer会发出通知“在已经注册的句柄集中,一个或多个句柄的事件已经就绪”;
    程序收到通知后,就能够在非阻塞的状况下对事件进行处理了。
对应到libevent中,依然是select、poll、epoll等,可是libevent使用结构体eventop进行了封装,以统一的接口来支持这些I/O多路复用机制,达到了对外隐藏底层系统机制的目的。

3) Reactor——反应器
    Reactor,是事件管理的接口,内部使用event demultiplexer注册、注销事件;并运行事件循环,当有事件进入“就绪”状态时,调用注册事件的回调函数处理事件。
对应到libevent中,就是event_base结构体。
一个典型的Reactor声明方式

1 class Reactor 2 { 3 public: 4     int register_handler(Event_Handler *pHandler, int event); 5     int remove_handler(Event_Handler *pHandler, int event); 6     void handle_events(timeval *ptv); 7     // ...
8 };

 

4) Event Handler——事件处理程序
    事件处理程序提供了一组接口,每一个接口对应了一种类型的事件,供Reactor在相应的事件发生时调用,执行相应的事件处理。一般它会绑定一个有效的句柄。
对应到libevent中,就是event结构体。
下面是两种典型的Event Handler类声明方式,两者互有优缺点。

 1 class Event_Handler  2 {  3 public:  4     virtual void handle_read() = 0;  5     virtual void handle_write() = 0;  6     virtual void handle_timeout() = 0;  7     virtual void handle_close() = 0;  8     virtual HANDLE get_handle() = 0;  9     // ...
10 }; 11 class Event_Handler 12 { 13 public: 14     // events maybe read/write/timeout/close .etc
15     virtual void handle_events(int events) = 0; 16     virtual HANDLE get_handle() = 0; 17     // ...
18 };

 

4 ,Reactor事件处理流程

前面说过Reactor将事件流“逆置”了,那么使用Reactor模式后,事件控制流是什么样子呢?
能够参见下面的序列图。

 

5 ,小结

上面讲到了Reactor的基本概念、框架和处理流程,对Reactor有个基本清晰的了解后,再来对比看libevent就会更容易理解了,接下来就正式进入到libevent的代码世界了,加油!

 

参考资料:
Pattern-Oriented Software Architecture, Patterns for Concurrent and Networked Objects, Volume 2

 

                                                                       第三章

1 ,前言

学习源代码该从哪里入手?我以为从程序的基本使用场景和代码的总体处理流程入手是个不错的方法,至少从我的的经验上讲,用此方法分析libevent是比较有效的。

2 ,基本应用场景

基本应用场景也是使用libevnet的基本流程,下面来考虑一个最简单的场景,使用livevent设置定时器,应用程序只须要执行下面几个简单的步骤便可。

1)首先初始化libevent库,并保存返回的指针

1 struct event_base * base = event_init();

实际上这一步至关于初始化一个Reactor实例;在初始化libevent后,就能够注册事件了。

 

2)初始化事件event,设置回调函数和关注的事件

1 evtimer_set(&ev, timer_cb, NULL);

事实上这等价于调用

1 event_set(&ev, -1, 0, timer_cb, NULL);

event_set的函数原型是:

1 void event_set(struct event *ev, int fd, short event, void (*cb)(int, short, void *), void *arg)

ev:执行要初始化的event对象;
fd:该event绑定的“句柄”,对于信号事件,它就是关注的信号;
event:在该fd上关注的事件类型,它能够是EV_READ, EV_WRITE, EV_SIGNAL;
cb:这是一个函数指针,当fd上的事件event发生时,调用该函数执行处理,它有三个参数,调用时由event_base负责传入,按顺序,实际上就是event_set时的fd, event和arg;
arg:传递给cb函数指针的参数;
因为定时事件不须要fd,而且定时事件是根据添加时(event_add)的超时值设定的,所以这里event也不须要设置。
这一步至关于初始化一个event handler,在libevent中事件类型保存在event结构体中。
注意:libevent并不会管理event事件集合,这须要应用程序自行管理;

 

3)设置event从属的event_base

1 event_base_set(base, &ev);  

这一步至关于指明event要注册到哪一个event_base实例上;

 

4)是正式的添加事件的时候了

1 event_add(&ev, timeout);

基本信息都已设置完成,只要简单的调用event_add()函数便可完成,其中timeout是定时值;
这一步至关于调用Reactor::register_handler()函数注册事件。

 

5)程序进入无限循环,等待就绪事件并执行事件处理

1 event_base_dispatch(base);

 

3 ,实例代码

上面例子的程序代码以下所示

 1 struct event ev;  2 struct timeval tv;  3 void time_cb(int fd, short event, void *argc)  4 {  5     printf("timer wakeup/n");  6     event_add(&ev, &tv); // reschedule timer
 7 }  8 int main()  9 { 10     struct event_base *base = event_init(); 11     tv.tv_sec = 10; // 10s period
12     tv.tv_usec = 0; 13     evtimer_set(&ev, time_cb, NULL); 14     event_add(&ev, &tv); 15     event_base_dispatch(base); 16 }

 

4 ,事件处理流程

当应用程序向libevent注册一个事件后,libevent内部是怎么样进行处理的呢?下面的图就给出了这一基本流程。
    1)首先应用程序准备并初始化event,设置好事件类型和回调函数;这对应于前面第步骤2和3;
    2)向libevent添加该事件event。对于定时事件,libevent使用一个小根堆管理,key为超时时间;对于Signal和I/O事件,libevent将其放入到等待链表(wait list)中,这是一个双向链表结构;
    3)程序调用event_base_dispatch()系列函数进入无限循环,等待事件,以select()函数为例;每次循环前libevent会检查定时事件的最小超时时间tv,根据tv设置select()的最大等待时间,以便于后面及时处理超         时事件;
当select()返回后,首先检查超时事件,而后检查I/O事件;
Libevent将全部的就绪事件,放入到激活链表中;
而后对激活链表中的事件,调用事件的回调函数执行事件处理;

 

5 ,小结

本节介绍了libevent的简单实用场景,并旋风般的介绍了libevent的事件处理流程,读者应该对libevent有了基本的印象,下面将会详细介绍libevent的事件管理框架(Reactor模式中的Reactor框架)作详细的介绍,在此以前会对源代码文件作简单的分类。

 

                                       第四章

libevent源代码文件组织

1 ,前言

详细分析源代码以前,若是能对其代码文件的基本结构有个大概的认识和分类,对于代码的分析将是大有裨益的。本节内容很少,我想并非说它不重要!

2 ,源代码组织结构

Libevent的源代码虽然都在一层文件夹下面,可是其代码分类仍是至关清晰的,主要可分为头文件、内部使用的头文件、辅助功能函数、日志、libevent框架、对系统I/O多路复用机制的封装、信号管理、定时事件管理、缓冲区管理、基本数据结构和基于libevent的两个实用库等几个部分,有些部分可能就是一个源文件。
源代码中的test部分就不在咱们关注的范畴了。
1)头文件
主要就是event.h:事件宏定义、接口函数声明,主要结构体event的声明;
2)内部头文件
xxx-internal.h:内部数据结构和函数,对外不可见,以达到信息隐藏的目的;
3)libevent框架
event.c:event总体框架的代码实现;
4)对系统I/O多路复用机制的封装
epoll.c:对epoll的封装;
select.c:对select的封装;
devpoll.c:对dev/poll的封装;
kqueue.c:对kqueue的封装;
5)定时事件管理
min-heap.h:其实就是一个以时间做为key的小根堆结构;
6)信号管理
signal.c:对信号事件的处理;
7)辅助功能函数
evutil.h 和evutil.c:一些辅助功能函数,包括建立socket pair和一些时间操做函数:加、减和比较等。
8)日志
log.h和log.c:log日志函数
9)缓冲区管理
evbuffer.c和buffer.c:libevent对缓冲区的封装;
10)基本数据结构
compat/sys下的两个源文件:queue.h是libevent基本数据结构的实现,包括链表,双向链表,队列等;_libevent_time.h:一些用于时间操做的结构体定义、函数和宏定义;
11)实用网络库
http和evdns:是基于libevent实现的http服务器和异步dns查询库;

 

3 ,小结

本节介绍了libevent的组织和分类,下面将会详细介绍libevent的核心部分event结构。

 

                                       第五章      libevent的核心:事件event

 

对事件处理流程有了高层的认识后,本节将详细介绍libevent的核心结构event,以及libevent对event的管理。

1 ,libevent的核心-event

  Libevent是基于事件驱动(event-driven)的,从名字也能够看到event是整个库的核心。event就是Reactor框架中的事件处理程序组件;它提供了函数接口,供Reactor在事件发生时调用,以执行相应的事件处理,一般它会绑定一个有效的句柄。
首先给出event结构体的声明,它位于event.h文件中:

 1 struct event {  2  TAILQ_ENTRY (event) ev_next;  3  TAILQ_ENTRY (event) ev_active_next;  4  TAILQ_ENTRY (event) ev_signal_next;  5  unsigned int min_heap_idx; /* for managing timeouts */
 6  struct event_base *ev_base;  7  int ev_fd;  8  short ev_events;  9  short ev_ncalls; 10  short *ev_pncalls; /* Allows deletes in callback */
11  struct timeval ev_timeout; 12  int ev_pri;  /* smaller numbers are higher priority */
13  void (*ev_callback)(int, short, void *arg); 14  void *ev_arg; 15  int ev_res;  /* result passed to event callback */
16  int ev_flags; 17 };

 

下面简单解释一下结构体中各字段的含义。
1)ev_events:event关注的事件类型,它能够是如下3种类型:
I/O事件: EV_WRITE和EV_READ
定时事件:EV_TIMEOUT
信号:    EV_SIGNAL
辅助选项:EV_PERSIST,代表是一个永久事件
Libevent中的定义为:

1 #define EV_TIMEOUT 0x01
2 #define EV_READ  0x02
3 #define EV_WRITE 0x04
4 #define EV_SIGNAL 0x08
5 #define EV_PERSIST 0x10 /* Persistant event */

 

能够看出事件类型可使用“|”运算符进行组合,须要说明的是,信号和I/O事件不能同时设置;
还能够看出libevent使用event结构体将这3种事件的处理统一块儿来;


2)ev_next,ev_active_next和ev_signal_next都是双向链表节点指针;它们是libevent对不一样事件类型和在不一样的时期,对事件的管理时使用到的字段。
libevent使用双向链表保存全部注册的I/O和Signal事件,ev_next就是该I/O事件在链表中的位置;称此链表为“已注册事件链表”;

一样ev_signal_next就是signal事件在signal事件链表中的位置;

ev_active_next:libevent将全部的激活事件放入到链表active list中,而后遍历active list执行调度,ev_active_next就指明了event在active list中的位置;


3)min_heap_idx和ev_timeout,若是是timeout事件,它们是event在小根堆中的索引和超时值,libevent使用小根堆来管理定时事件,这将在后面定时事件处理时专门讲解


4)ev_base该事件所属的反应堆实例,这是一个event_base结构体,下一节将会详细讲解;


5)ev_fd,对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号;


6)ev_callback,event的回调函数,被ev_base调用,执行事件处理程序,这是一个函数指针,原型为:

1 void (*ev_callback)(int fd, short events, void *arg)

其中参数fd对应于ev_fd;events对应于ev_events;arg对应于ev_arg;

 

7)ev_arg:void*,代表能够是任意类型的数据,在设置event时指定;


8)eb_flags:libevent用于标记event信息的字段,代表其当前的状态,可能的值有:

1 #define EVLIST_TIMEOUT 0x01 // event在time堆中
2 #define EVLIST_INSERTED 0x02 // event在已注册事件链表中
3 #define EVLIST_SIGNAL 0x04 // 未见使用
4 #define EVLIST_ACTIVE 0x08 // event在激活链表中
5 #define EVLIST_INTERNAL 0x10 // 内部使用标记
6 #define EVLIST_INIT     0x80 // event已被初始化

 

9)ev_ncalls:事件就绪执行时,调用ev_callback的次数,一般为1;


10)ev_pncalls:指针,一般指向ev_ncalls或者为NULL;


11)ev_res:记录了当前激活事件的类型;

 

2 ,libevent对event的管理

从event结构体中的3个链表节点指针和一个堆索引出发,大致上也能窥出libevent对event的管理方法了,能够参见下面的示意图:

 

每次当有事件event转变为就绪状态时,libevent就会把它移入到active event list[priority]中,其中priority是event的优先级;

接着libevent会根据本身的调度策略选择就绪事件,调用其cb_callback()函数执行事件处理;并根据就绪的句柄和事件类型填充cb_callback函数的参数。

 

3 ,事件设置的接口函数

要向libevent添加一个事件,须要首先设置event对象,这经过调用libevent提供的函数有:event_set(), event_base_set(), event_priority_set()来完成;下面分别进行讲解。

1 void event_set(struct event *ev, int fd, short events,  void (*callback)(int, short, void *), void *arg)

1.设置事件ev绑定的文件描述符或者信号,对于定时事件,设为-1便可;
2.设置事件类型,好比EV_READ|EV_PERSIST, EV_WRITE, EV_SIGNAL等;
3.设置事件的回调函数以及参数arg;
4.初始化其它字段,好比缺省的event_base和优先级;

 

1 int event_base_set(struct event_base *base, struct event *ev)

设置event ev将要注册到的event_base;
libevent有一个全局event_base指针current_base,默认状况下事件ev将被注册到current_base上,使用该函数能够指定不一样的event_base;
若是一个进程中存在多个libevent实例,则必需要调用该函数为event设置不一样的event_base;

 

1 int event_priority_set(struct event *ev, int pri)

设置event ev的优先级,没什么可说的,注意的一点就是:当ev正处于就绪状态时,不能设置,返回-1。

 

4 ,小结

 本节讲述了libevent的核心event结构,以及libevent支持的事件类型和libevent对event的管理模型;接下来将会描述libevent的事件处理框架,以及其中使用的重要的结构体event_base;

 

                                       第六章   初见事件处理框架

 

前面已经对libevent的事件处理框架和event结构体作了描述,如今是时候剖析libevent对事件的详细处理流程了,本节将分析libevent的事件处理框架event_base和libevent注册、删除事件的具体流程,可结合前一节libevent对event的管理。

1 ,事件处理框架-event_base

回想Reactor模式的几个基本组件,本节讲解的部分对应于Reactor框架组件。在libevent中,这就表现为event_base结构体,结构体声明以下,它位于event-internal.h文件中:

 1 struct event_base {  2  const struct eventop *evsel;  3  void *evbase;   4  int event_count;  /* counts number of total events */
 5  int event_count_active; /* counts number of active events */
 6  int event_gotterm;  /* Set to terminate loop */
 7  int event_break;  /* Set to terminate loop immediately */
 8  /* active event management */
 9  struct event_list **activequeues; 10  int nactivequeues; 11  /* signal handling info */
12  struct evsignal_info sig; 13  struct event_list eventqueue; 14  struct timeval event_tv; 15  struct min_heap timeheap; 16  struct timeval tv_cache; 17 };

 

下面详细解释一下结构体中各字段的含义。


1)evsel和evbase这两个字段的设置可能会让人有些迷惑,这里你能够把evsel和evbase看做是类和静态函数的关系,

好比添加事件时的调用行为:evsel->add(evbase, ev),实际执行操做的是evbase;这至关于class::add(instance, ev),instance就是class的一个对象实例。
evsel指向了全局变量static const struct eventop *eventops[]中的一个;

前面也说过,libevent将系统提供的I/O demultiplex机制统一封装成了eventop结构;所以eventops[]包含了select、poll、kequeue和epoll等等其中的若干个全局实例对象。
evbase其实是一个eventop实例对象;


先来看看eventop结构体,它的成员是一系列的函数指针, 在event-internal.h文件中:

 1 struct eventop {  2  const char *name;  3  void *(*init)(struct event_base *); // 初始化
 4  int (*add)(void *, struct event *); // 注册事件
 5  int (*del)(void *, struct event *); // 删除事件
 6  int (*dispatch)(struct event_base *, void *, struct timeval *); // 事件分发
 7  void (*dealloc)(struct event_base *, void *); // 注销,释放资源
 8  /* set if we need to reinitialize the event base */
 9  int need_reinit; 10 };

 

也就是说,在libevent中,每种I/O demultiplex机制的实现都必须提供这五个函数接口,来完成自身的初始化、销毁释放;对事件的注册、注销和分发。
好比对于epoll,libevent实现了5个对应的接口函数,并在初始化时并将eventop的5个函数指针指向这5个函数,那么程序就可使用epoll做为I/O demultiplex机制了,这个在后面会再次提到。


2)activequeues是一个二级指针,前面讲过libevent支持事件优先级,所以你能够把它看做是数组,其中的元素activequeues[priority]是一个链表,链表的每一个节点指向一个优先级为priority的就绪事件event。

 
3)eventqueue,链表,保存了全部的注册事件event的指针。


4)sig是由来管理信号的结构体,将在后面信号处理时专门讲解;


5)timeheap是管理定时事件的小根堆,将在后面定时事件处理时专门讲解;


6)event_tv和tv_cache是libevent用于时间管理的变量,将在后面讲到;
其它各个变量都能因名知意,就再也不啰嗦了。

 

2 ,建立和初始化event_base

建立一个event_base对象也既是建立了一个新的libevent实例,程序须要经过调用event_init()(内部调用event_base_new函数执行具体操做)函数来建立,该函数同时还对新生成的libevent实例进行了初始化。
该函数首先为event_base实例申请空间,而后初始化timer mini-heap,选择并初始化合适的系统I/O 的demultiplexer机制,初始化各事件链表;
函数还检测了系统的时间设置,为后面的时间管理打下基础。

 

3 ,接口函数

前面提到Reactor框架的做用就是提供事件的注册、注销接口;根据系统提供的事件多路分发机制执行事件循环,当有事件进入“就绪”状态时,调用注册事件的回调函数来处理事件。
Libevent中对应的接口函数主要就是:

1 int  event_add(struct event *ev, const struct timeval *timeout); 2 int  event_del(struct event *ev); 3 int  event_base_loop(struct event_base *base, int loops); 4 void event_active(struct event *event, int res, short events); 5 void event_process_active(struct event_base *base); 

 

本节将按介绍事件注册和删除的代码流程,libevent的事件循环框架将在下一节再具体描述。
对于定时事件,这些函数将调用timer heap管理接口执行插入和删除操做;对于I/O和Signal事件将调用eventopadd和delete接口函数执行插入和删除操做(eventop会对Signal事件调用Signal处理接口执行操做);这些组件将在后面的内容描述。

 

1)注册事件
函数原型:

1 int event_add(struct event *ev, const struct timeval *tv)

参数:

    ev:指向要注册的事件;
    tv:超时时间;
函数将ev注册到ev->ev_base上,事件类型由ev->ev_events指明,若是注册成功,ev将被插入到已注册链表中;若是tv不是NULL,则会同时注册定时事件,将ev添加到timer堆上;
若是其中有一步操做失败,那么函数保证没有事件会被注册,能够讲这至关于一个原子操做。这个函数也体现了libevent细节之处的巧妙设计,且仔细看程序代码,部分有省略,注释直接附在代码中。

 1 int event_add(struct event *ev, const struct timeval *tv)  2 {  3  struct event_base *base = ev->ev_base; // 要注册到的event_base
 4  const struct eventop *evsel = base->evsel;  5  void *evbase = base->evbase; // base使用的系统I/O策略  6  // 新的timer事件,调用timer heap接口在堆上预留一个位置  7  // 注:这样能保证该操做的原子性:  8  // 向系统I/O机制注册可能会失败,而当在堆上预留成功后,  9  // 定时事件的添加将确定不会失败; 10  // 而预留位置的可能结果是堆扩充,可是内部元素并不会改变
11  if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) { 12   if (min_heap_reserve(&base->timeheap, 13    1 + min_heap_size(&base->timeheap)) == -1) 14    return (-1);  /* ENOMEM == errno */
15  } 16  // 若是事件ev不在已注册或者激活链表中,则调用evbase注册事件
17  if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
18   !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) { 19    res = evsel->add(evbase, ev); 20    if (res != -1) // 注册成功,插入event到已注册链表中
21     event_queue_insert(base, ev, EVLIST_INSERTED); 22  } 23  // 准备添加定时事件
24  if (res != -1 && tv != NULL) { 25   struct timeval now; 26   // EVLIST_TIMEOUT代表event已经在定时器堆中了,删除旧的
27   if (ev->ev_flags & EVLIST_TIMEOUT) 28    event_queue_remove(base, ev, EVLIST_TIMEOUT); 29   // 若是事件已是就绪状态则从激活链表中删除
30   if ((ev->ev_flags & EVLIST_ACTIVE) &&
31    (ev->ev_res & EV_TIMEOUT)) { 32     // 将ev_callback调用次数设置为0
33     if (ev->ev_ncalls && ev->ev_pncalls) { 34      *ev->ev_pncalls = 0; 35  } 36     event_queue_remove(base, ev, EVLIST_ACTIVE); 37  } 38   // 计算时间,并插入到timer小根堆中
39   gettime(base, &now); 40   evutil_timeradd(&now, tv, &ev->ev_timeout); 41   event_queue_insert(base, ev, EVLIST_TIMEOUT); 42  } 43  return (res); 44 } 45  
46 event_queue_insert()负责将事件插入到对应的链表中,下面是程序代码; 47 event_queue_remove()负责将事件从对应的链表中删除,这里就再也不重复贴代码了; 48 void event_queue_insert(struct event_base *base, struct event *ev, int queue) 49 { 50  // ev可能已经在激活列表中了,避免重复插入
51  if (ev->ev_flags & queue) { 52   if (queue & EVLIST_ACTIVE) 53    return; 54  } 55  // ...
56  ev->ev_flags |= queue; // 记录queue标记
57  switch (queue) { 58  case EVLIST_INSERTED: // I/O或Signal事件,加入已注册事件链表
59   TAILQ_INSERT_TAIL(&base->eventqueue, ev, ev_next); 60   break; 61  case EVLIST_ACTIVE: // 就绪事件,加入激活链表
62   base->event_count_active++; 63   TAILQ_INSERT_TAIL(base->activequeues[ev->ev_pri], ev, ev_active_next); 64   break; 65  case EVLIST_TIMEOUT: // 定时事件,加入堆
66   min_heap_push(&base->timeheap, ev); 67   break; 68  } 69 }

 

2)删除事件:

函数原型为:

1 int  event_del(struct event *ev);

函数将删除事件ev,对于I/O事件,从I/O 的demultiplexer上将事件注销;对于Signal事件,将从Signal事件链表中删除;对于定时事件,将从堆上删除;
一样删除事件的操做则不必定是原子的,好比删除时间事件以后,有可能从系统I/O机制中注销会失败。

 1 int event_del(struct event *ev)  2 {  3  struct event_base *base;  4  const struct eventop *evsel;  5  void *evbase;  6  // ev_base为NULL,代表ev没有被注册
 7  if (ev->ev_base == NULL)  8   return (-1);  9  // 取得ev注册的event_base和eventop指针
10  base = ev->ev_base; 11  evsel = base->evsel; 12  evbase = base->evbase; 13  // 将ev_callback调用次数设置为
14  if (ev->ev_ncalls && ev->ev_pncalls) { 15   *ev->ev_pncalls = 0; 16  } 17  
18  // 从对应的链表中删除
19  if (ev->ev_flags & EVLIST_TIMEOUT) 20   event_queue_remove(base, ev, EVLIST_TIMEOUT); 21  if (ev->ev_flags & EVLIST_ACTIVE) 22   event_queue_remove(base, ev, EVLIST_ACTIVE); 23  if (ev->ev_flags & EVLIST_INSERTED) { 24   event_queue_remove(base, ev, EVLIST_INSERTED); 25   // EVLIST_INSERTED代表是I/O或者Signal事件, 26   // 须要调用I/O demultiplexer注销事件
27   return (evsel->del(evbase, ev)); 28  } 29  return (0); 30 }

 

4 ,小节
分析了event_base这一重要结构体,初步看到了libevent对系统的I/O demultiplex机制的封装event_op结构,并结合源代码分析了事件的注册和删除处理,下面将会接着分析事件管理框架中的主事件循环部分。

 

                                       第七章  事件主循环

 

  如今咱们已经初步了解了libevent的Reactor组件——event_base和事件管理框架,接下来就是libevent事件处理的中心部分——事件主循环,根据系统提供的事件多路分发机制执行事件循环,对已注册的就绪事件,调用注册事件的回调函数来处理事件。

1 ,阶段性的胜利

Libevent的事件主循环主要是经过event_base_loop ()函数完成的,其主要操做以下面的流程图所示,event_base_loop所做的就是持续执行下面的循环。

 

清楚了event_base_loop所做的主要操做,就能够对比源代码看个究竟了,代码结构仍是至关清晰的。

 1 int event_base_loop(struct event_base *base, int flags)  2 {  3     const struct eventop *evsel = base->evsel;  4     void *evbase = base->evbase;  5     struct timeval tv;  6     struct timeval *tv_p;  7     int res, done;  8     // 清空时间缓存
 9     base->tv_cache.tv_sec = 0; 10     // evsignal_base是全局变量,在处理signal时,用于指名signal所属的event_base实例
11     if (base->sig.ev_signal_added) 12         evsignal_base = base; 13     done = 0; 14     while (!done) { // 事件主循环 15         // 查看是否须要跳出循环,程序能够调用event_loopexit_cb()设置event_gotterm标记 16         // 调用event_base_loopbreak()设置event_break标记
17         if (base->event_gotterm) { 18             base->event_gotterm = 0; 19             break; 20  } 21         if (base->event_break) { 22             base->event_break = 0; 23             break; 24  } 25         // 校订系统时间,若是系统使用的是非MONOTONIC时间,用户可能会向后调整了系统时间 26         // 在timeout_correct函数里,比较last wait time和当前时间,若是当前时间< last wait time 27         // 代表时间有问题,这是须要更新timer_heap中全部定时事件的超时时间。
28         timeout_correct(base, &tv); 29    
30         // 根据timer heap中事件的最小超时时间,计算系统I/O demultiplexer的最大等待时间
31         tv_p = &tv; 32         if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) { 33             timeout_next(base, &tv_p); 34         } else { 35             // 依然有未处理的就绪时间,就让I/O demultiplexer当即返回,没必要等待 36             // 下面会提到,在libevent中,低优先级的就绪事件可能不能当即被处理
37             evutil_timerclear(&tv); 38  } 39         // 若是当前没有注册事件,就退出
40         if (!event_haveevents(base)) { 41             event_debug(("%s: no events registered.", __func__)); 42             return (1); 43  } 44         // 更新last wait time,并清空time cache
45         gettime(base, &base->event_tv); 46         base->tv_cache.tv_sec = 0; 47         // 调用系统I/O demultiplexer等待就绪I/O events,多是epoll_wait,或者select等; 48         // 在evsel->dispatch()中,会把就绪signal event、I/O event插入到激活链表中
49         res = evsel->dispatch(base, evbase, tv_p); 50         if (res == -1) 51             return (-1); 52         // 将time cache赋值为当前系统时间
53         gettime(base, &base->tv_cache); 54         // 检查heap中的timer events,将就绪的timer event从heap上删除,并插入到激活链表中
55         timeout_process(base); 56         // 调用event_process_active()处理激活链表中的就绪event,调用其回调函数执行事件处理 57         // 该函数会寻找最高优先级(priority值越小优先级越高)的激活事件链表, 58         // 而后处理链表中的全部就绪事件; 59         // 所以低优先级的就绪事件可能得不到及时处理;
60         if (base->event_count_active) { 61             event_process_active(base); 62             if (!base->event_count_active && (flags & EVLOOP_ONCE)) 63                 done = 1; 64         } else if (flags & EVLOOP_NONBLOCK) 65             done = 1; 66  } 67     // 循环结束,清空时间缓存
68     base->tv_cache.tv_sec = 0; 69     event_debug(("%s: asked to terminate loop.", __func__)); 70     return (0); 71 }

 

3 ,I/O和Timer事件的统一

    Libevent将Timer和Signal事件都统一到了系统的I/O 的demultiplex机制中了,相信读者从上面的流程和代码中也能窥出一斑了,下面就再啰嗦一次了。


     首先将Timer事件融合到系统I/O多路复用机制中,仍是至关清晰的,由于系统的I/O机制像select()和epoll_wait()都容许程序制定一个最大等待时间(也称为最大超时时间)timeout,即便没有I/O事件发生,它们也保证能在timeout时间内返回。

     那么根据全部Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活全部就绪的Timer事件就能够了,这样就能将Timer事件完美的融合到系统的I/O机制中了。
     这是在Reactor和Proactor模式(主动器模式,好比Windows上的IOCP)中处理Timer事件的经典方法了,ACE采用的也是这种方法,你们能够参考POSA vol2书中的Reactor模式一节。
     堆是一种经典的数据结构,向堆中插入、删除元素时间复杂度都是O(lgN),N为堆中元素的个数,而获取最小key值(小根堆)的复杂度为O(1);所以变成了管理Timer事件的绝佳人选(固然是非惟一的),libevent就是采用的堆结构。

 

4 ,I/O和Signal事件的统一

     Signal是异步事件的经典事例,将Signal事件统一到系统的I/O多路复用中就不像Timer事件那么天然了,Signal事件的出现对于进程来说是彻底随机的,进程不能只是测试一个变量来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行以下的操做”。
     若是当Signal发生时,并不当即调用event的callback函数处理信号,而是设法通知系统的I/O机制,让其返回,而后再统一和I/O事件以及Timer一块儿处理,不就能够了嘛。是的,这也是libevent中使用的方法。
     问题的核心在于,当Signal发生时,如何通知系统的I/O多路复用机制,这里先买个小关子,放到信号处理一节再详细说明,我想读者确定也能想出通知的方法,好比使用pipe。

 

5 ,小节

介绍了libevent的事件主循环,描述了libevent是如何处理就绪的I/O事件、定时器和信号事件,以及如何将它们无缝的融合到一块儿。

 

                                       第八章    集成信号处理

 

如今咱们已经了解了libevent的基本框架:事件管理框架和事件主循环。上节提到了libevent中I/O事件和Signal以及Timer事件的集成,这一节将分析如何将Signal集成到事件主循环的框架中。

1 ,集成策略——使用socket pair

前一节已经作了足够多的介绍了,基本方法就是采用“消息机制”。在libevent中这是经过socket pair完成的,下面就来详细分析一下。
Socket pair就是一个socket对,包含两个socket,一个读socket,一个写socket。工做方式以下图所示:

 

建立一个socket pair并非复杂的操做,能够参见下面的流程图,清晰起见,其中忽略了一些错误处理和检查。

 

Libevent提供了辅助函数evutil_socketpair()来建立一个socket pair,能够结合上面的建立流程来分析该函数。

 

2 ,集成到事件主循环——通知event_base

      Socket pair建立好了,但是libevent的事件主循环仍是不知道Signal是否发生了啊,看来咱们还差了最后一步,那就是:为socket pair的读socket在libevent的event_base实例上注册一个persist的读事件。
      这样当向写socket写入数据时,读socket就会获得通知,触发读事件,从而event_base就能相应的获得通知了。
      前面提到过,Libevent会在事件主循环中检查标记,来肯定是否有触发的signal,若是标记被设置就处理这些signal,这段代码在各个具体的I/O机制中,以Epoll为例,在epoll_dispatch()函数中,代码片断如           下:

 1     res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
 2     if (res == -1) {
 3         if (errno != EINTR) {
 4             event_warn("epoll_wait");
 5             return (-1);
 6         }
 7         evsignal_process(base);// 处理signal事件
 8         return (0);
 9     } else if (base->sig.evsignal_caught) {
10         evsignal_process(base);// 处理signal事件
11     }

 

完整的处理框架以下所示:

 

注1:libevent中,初始化阶段并不注册读socket的读事件,而是在注册信号阶段才会测试并注册;
注2:libevent中,检查I/O事件是在各系统I/O机制的dispatch()函数中完成的,该dispatch()函数在event_base_loop()函数中被调用;

 

3 ,evsignal_info结构体

Libevent中Signal事件的管理是经过结构体evsignal_info完成的,结构体位于evsignal.h文件中,定义以下:

 1 struct evsignal_info {
 2     struct event ev_signal;
 3     int ev_signal_pair[2];
 4     int ev_signal_added;
 5     volatile sig_atomic_t evsignal_caught;
 6     struct event_list evsigevents[NSIG];
 7     sig_atomic_t evsigcaught[NSIG];
 8 #ifdef HAVE_SIGACTION
 9     struct sigaction **sh_old;
10 #else
11     ev_sighandler_t **sh_old;
12 #endif
13     int sh_old_max;
14 };

 

下面详细介绍一下个字段的含义和做用:
1)ev_signal, 为socket pair的读socket向event_base注册读事件时使用的event结构体;
2)ev_signal_pair,socket pair对,做用见第一节的介绍;
3)ev_signal_added,记录ev_signal事件是否已经注册了;
4)evsignal_caught,是否有信号发生的标记;是volatile类型,由于它会在另外的线程中被修改;
5)evsigvents[NSIG],数组,evsigevents[signo]表示注册到信号signo的事件链表;
6)evsigcaught[NSIG],具体记录每一个信号触发的次数,evsigcaught[signo]是记录信号signo被触发的次数;
7)sh_old记录了原来的signal处理函数指针,当信号signo注册的event被清空时,须要从新设置其处理函数;
    evsignal_info的初始化包括,建立socket pair,设置ev_signal事件(但并无注册,而是等到有信号注册时才检查并注册),并将全部标记置零,初始化信号的注册事件链表指针等。

 

4 ,注册、注销signal事件

注册signal事件是经过evsignal_add(struct event *ev)函数完成的,libevent对全部的信号注册同一个处理函数evsignal_handler(),该函数将在下一段介绍,注册过程以下:
1 取得ev要注册到的信号signo;
2 若是信号signo未被注册,那么就为signo注册信号处理函数evsignal_handler();
3 若是事件ev_signal还没哟注册,就注册ev_signal事件;
4 将事件ev添加到signo的event链表中;
从signo上注销一个已注册的signal事件就更简单了,直接从其已注册事件的链表中移除便可。若是事件链表已空,那么就恢复旧的处理函数;
下面的讲解都以signal()函数为例,sigaction()函数的处理和signal()类似。
处理函数evsignal_handler()函数作的事情很简单,就是记录信号的发生次数,并通知event_base有信号触发,须要处理:

 1 static void evsignal_handler(int sig)
 2 {
 3     int save_errno = errno; // 不覆盖原来的错误代码
 4     if (evsignal_base == NULL) {
 5         event_warn("%s: received signal %d, but have no base configured", __func__, sig);
 6         return;
 7     }
 8     // 记录信号sig的触发次数,并设置event触发标记
 9     evsignal_base->sig.evsigcaught[sig]++;
10     evsignal_base->sig.evsignal_caught = 1;
11 #ifndef HAVE_SIGACTION
12     signal(sig, evsignal_handler); // 从新注册信号
13 #endif
14     // 向写socket写一个字节数据,触发event_base的I/O事件,从而通知其有信号触发,须要处理
15     send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
16     errno = save_errno; // 错误代码
17 }

 

5,小节

本节介绍了libevent对signal事件的具体处理框架,包括事件注册、删除和socket pair通知机制,以及是如何将Signal事件集成到事件主循环之中的。

 

                                       第九章   集成定时器事件

 

如今再来详细分析libevent中I/O事件和Timer事件的集成,与Signal相比,Timer事件的集成会直观和简单不少。Libevent对堆的调整操做作了一些优化,本节还会描述这些优化方法。

1,集成到事件主循环

     由于系统的I/O机制像select()和epoll_wait()都容许程序制定一个最大等待时间(也称为最大超时时间)timeout,即便没有I/O事件发生,它们也保证能在timeout时间内返回。
     那么根据全部Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活全部就绪的Timer事件就能够了,这样就能将Timer事件完美的融合到系统的I/O机制中了。
     具体的代码在源文件event.c的event_base_loop()中,如今就对比代码来看看这一处理方法:

 1         if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
 2             // 根据Timer事件计算evsel->dispatch的最大等待时间
 3             timeout_next(base, &tv_p);
 4         } else { 
 5             // 若是还有活动事件,就不要等待,让evsel->dispatch当即返回
 6             evutil_timerclear(&tv);
 7         }
 8         // ...
 9         // 调用select() or epoll_wait() 等待就绪I/O事件
10         res = evsel->dispatch(base, evbase, tv_p);
11         // ...
12         // 处理超时事件,将超时事件插入到激活链表中
13         timeout_process(base);

 

 timeout_next()函数根据堆中具备最小超时值的事件和当前时间来计算等待时间,下面看看代码:

 1 static int timeout_next(struct event_base *base, struct timeval **tv_p)
 2 {
 3     struct timeval now;
 4     struct event *ev;
 5     struct timeval *tv = *tv_p;
 6     // 堆的首元素具备最小的超时值
 7     if ((ev = min_heap_top(&base->timeheap)) == NULL) {
 8         // 若是没有定时事件,将等待时间设置为NULL,表示一直阻塞直到有I/O事件发生
 9         *tv_p = NULL;
10         return (0);
11     }
12     // 取得当前时间
13     gettime(base, &now);
14     // 若是超时时间<=当前值,不能等待,须要当即返回
15     if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {
16         evutil_timerclear(tv);
17         return (0);
18     }
19     // 计算等待的时间=当前时间-最小的超时时间
20     evutil_timersub(&ev->ev_timeout, &now, tv);
21     return (0);
22 }

 

2, Timer小根堆

      Libevent使用堆来管理Timer事件,其key值就是事件的超时时间,源代码位于文件min_heap.h中。
全部的数据结构书中都有关于堆的详细介绍,向堆中插入、删除元素时间复杂度都是O(lgN),N为堆中元素的个数,而获取最小key值(小根堆)的复杂度为O(1)。堆是一个彻底二叉树,基本存储方式是一个数组。
      Libevent实现的堆仍是比较轻巧的,虽然我不喜欢这种编码方式(搞一些复杂的表达式)。轻巧到什么地方呢,就以插入元素为例,来对比说明,下面伪代码中的size表示当前堆的元素个数:
典型的代码逻辑以下:

 1 Heap[size++] = new; // 先放到数组末尾,元素个数+1
 2 // 下面就是shift_up()的代码逻辑,不断的将new向上调整
 3 _child = size;
 4 while(_child>0) // 循环
 5 {
 6    _parent = (_child-1)/2; // 计算parent
 7    if(Heap[_parent].key < Heap[_child].key)
 8       break; // 调整结束,跳出循环
 9    swap(_parent, _child); // 交换parent和child
10 }

 

     而libevent的heap代码对这一过程作了优化,在插入新元素时,只是为新元素预留了一个位置hole(初始时hole位于数组尾部),但并不马上将新元素插入到hole上,而是不断向上调整hole的值,将父节点向下调整,最后确认hole就是新元素的所在位置时,才会真正的将新元素插入到hole上,所以在调整过程当中就比上面的代码少了一次赋值的操做,代码逻辑是:
     下面就是shift_up()的代码逻辑,不断的将new的“预留位置”向上调整

 1 // 下面就是shift_up()的代码逻辑,不断的将new的“预留位置”向上调整
 2 _hole = size; // _hole就是为new预留的位置,但并不马上将new放上
 3 while(_hole>0) // 循环
 4 {
 5     _parent = (_hole-1)/2; // 计算parent
 6     if(Heap[_parent].key < new.key)
 7         break; // 调整结束,跳出循环
 8     Heap[_hole] = Heap[_parent]; // 将parent向下调整
 9     _hole = _parent; // 将_hole调整到_parent
10 }
11 Heap[_hole] = new; // 调整结束,将new插入到_hole指示的位置
12 size++; // 元素个数+1

 

因为每次调整都少作一次赋值操做,在调整路径比较长时,调整效率会比第一种有所提升。libevent中的min_heap_shift_up_()函数就是上面逻辑的具体实现,对应的向下调整函数是min_heap_shift_down_()。
举个例子,向一个小根堆3, 5, 8, 7, 12中插入新元素2,使用第一中典型的代码逻辑,其调整过程以下图所示:

 

使用libevent中的堆调整逻辑,调整过程以下图所示:

 

对于删除和元素修改操做,也听从相同的逻辑,就再也不罗嗦了。

 

3, 小节

经过设置系统I/O机制的wait时间,从而简捷的集成Timer事件;主要分析了libevent对堆调整操做的优化。

 

                                       第十章   支持I/O多路复用技术

 

Libevent的核心是事件驱动、同步非阻塞,为了达到这一目标,必须采用系统提供的I/O多路复用技术,而这些在Windows、Linux、Unix等不一样平台上却各有不一样,如何能提供优雅而统一的支持方式,是首要关键的问题,这其实不难,本节就来分析一下。

1, 统一的关键

 Libevent支持多种I/O多路复用技术的关键就在于结构体eventop,这个结构体前面也曾提到过,它的成员是一系列的函数指针, 定义在event-internal.h文件中:

 1 struct eventop {
 2     const char *name;
 3     void *(*init)(struct event_base *); // 初始化
 4     int (*add)(void *, struct event *); // 注册事件
 5     int (*del)(void *, struct event *); // 删除事件
 6     int (*dispatch)(struct event_base *, void *, struct timeval *); // 事件分发
 7     void (*dealloc)(struct event_base *, void *); // 注销,释放资源
 8     /* set if we need to reinitialize the event base */
 9     int need_reinit;
10 };

 

在libevent中,每种I/O demultiplex机制的实现都必须提供这五个函数接口,来完成自身的初始化、销毁释放;对事件的注册、注销和分发。
好比对于epoll,libevent实现了5个对应的接口函数,并在初始化时并将eventop的5个函数指针指向这5个函数,那么程序就可使用epoll做为I/O demultiplex机制了。

 

2, 设置I/O demultiplex机制

Libevent把全部支持的I/O demultiplex机制存储在一个全局静态数组eventops中,并在初始化时选择使用何种机制,数组内容根据优先级顺序声明以下:

 1 /* In order of preference */
 2 static const struct eventop *eventops[] = {
 3 #ifdef HAVE_EVENT_PORTS
 4     &evportops,
 5 #endif
 6 #ifdef HAVE_WORKING_KQUEUE
 7     &kqops,
 8 #endif
 9 #ifdef HAVE_EPOLL
10     &epollops,
11 #endif
12 #ifdef HAVE_DEVPOLL
13     &devpollops,
14 #endif
15 #ifdef HAVE_POLL
16     &pollops,
17 #endif
18 #ifdef HAVE_SELECT
19     &selectops,
20 #endif
21 #ifdef WIN32
22     &win32ops,
23 #endif
24     NULL
25 }; 

 

 而后libevent根据系统配置和编译选项决定使用哪种I/O demultiplex机制,这段代码在函数event_base_new()中:

1 base->evbase = NULL;
2     for (i = 0; eventops[i] && !base->evbase; i++) {
3         base->evsel = eventops[i];
4         base->evbase = base->evsel->init(base);
5     }&nbsp;

 

能够看出,libevent在编译阶段选择系统的I/O demultiplex机制,而不支持在运行阶段根据配置再次选择。
 以Linux下面的epoll为例,实如今源文件epoll.c中,eventops对象epollops定义以下:

1 const struct eventop epollops = {
2     "epoll",
3     epoll_init,
4     epoll_add,
5     epoll_del,
6     epoll_dispatch,
7     epoll_dealloc,
8     1 /* need reinit */
9 };

 

变量epollops中的函数指针具体声明以下,注意到其返回值和参数都和eventop中的定义严格一致,这是函数指针的语法限制。

1 static void *epoll_init    (struct event_base *);
2 static int epoll_add    (void *, struct event *);
3 static int epoll_del    (void *, struct event *);
4 static int epoll_dispatch(struct event_base *, void *, struct timeval *);
5 static void epoll_dealloc    (struct event_base *, void *);

 

那么若是选择的是epoll,那么调用结构体eventop的init和dispatch函数指针时,实际调用的函数就是epoll的初始化函数epoll_init()和事件分发函数epoll_dispatch()了;
关于epoll的具体用法这里就很少说了,能够参见介绍epoll的文章(本人的哈哈):http://blog.csdn.net/sparkliang/archive/2009/11/05/4770655.aspx

 

C++语言提供了虚函数来实现多态,在C语言中,这是经过函数指针实现的。对于各种函数指针的详细说明能够参见文章:http://blog.csdn.net/sparkliang/archive/2009/06/09/4254115.aspx

一样的,上面epollops以及epoll的各类函数都直接定义在了epoll.c源文件中,对外都是不可见的。对于libevent的使用者而言,彻底不会知道它们的存在,对epoll的使用也是经过eventop来完成的,达到了信息隐藏的目的。

 

3, 小节

支持多种I/O demultiplex机制的方法其实挺简单的,借助于函数指针就OK了。经过对源代码的分析也能够看出,Libevent是在编译阶段选择系统的I/O demultiplex机制的,而不支持在运行阶段根据配置再次选择。

 

                                       第十一章   时间管理

 

为了支持定时器,Libevent必须和系统时间打交道,这一部分的内容也比较简单,主要涉及到时间的加减辅助函数、时间缓存、时间校订和定时器堆的时间值调整等。下面就结合源代码来分析一下。

1, 初始化检测

Libevent在初始化时会检测系统时间的类型,经过调用函数detect_monotonic()完成,它经过调用clock_gettime()来检测系统是否支持monotonic时钟类型:

1 static void detect_monotonic(void)
2 {
3 #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
4     struct timespec    ts;
5     if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
6         use_monotonic = 1; // 系统支持monotonic时间
7 #endif
8 }

 

Monotonic时间指示的是系统从boot后到如今所通过的时间,若是系统支持Monotonic时间就将全局变量use_monotonic设置为1,设置use_monotonic到底有什么用,这个在后面说到时间校订时就能看出来了。

 

2, 时间缓存

 结构体event_base中的tv_cache,用来记录时间缓存。这个还要从函数gettime()提及,先来看看该函数的代码:

 1 static int gettime(struct event_base *base, struct timeval *tp)
 2 {
 3     // 若是tv_cache时间缓存已设置,就直接使用
 4     if (base->tv_cache.tv_sec) {
 5         *tp = base->tv_cache;
 6         return (0);
 7     }
 8     // 若是支持monotonic,就用clock_gettime获取monotonic时间
 9 #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
10     if (use_monotonic) {
11         struct timespec    ts;
12         if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1)
13             return (-1);
14         tp->tv_sec = ts.tv_sec;
15         tp->tv_usec = ts.tv_nsec / 1000;
16         return (0);
17     }
18 #endif
19     // 不然只能取得系统当前时间
20     return (evutil_gettimeofday(tp, NULL));
21 }

 

     若是tv_cache已经设置,那么就直接使用缓存的时间;不然须要再次执行系统调用获取系统时间。
     函数evutil_gettimeofday()用来获取当前系统时间,在Linux下其实就是系统调用gettimeofday();Windows没有提供函数gettimeofday,而是经过调用_ftime()来完成的。
     在每次系统事件循环中,时间缓存tv_cache将会被相应的清空和设置,再次来看看下面event_base_loop的主要代码逻辑:

 1 int event_base_loop(struct event_base *base, int flags)
 2 {
 3     // 清空时间缓存
 4     base->tv_cache.tv_sec = 0;
 5     while(!done){
 6         timeout_correct(base, &tv); // 时间校订
 7         // 更新event_tv到tv_cache指示的时间或者当前时间(第一次)
 8          // event_tv <--- tv_cache
 9         gettime(base, &base->event_tv);
10         // 清空时间缓存-- 时间点1
11         base->tv_cache.tv_sec = 0;
12         // 等待I/O事件就绪
13         res = evsel->dispatch(base, evbase, tv_p);
14         // 缓存tv_cache存储了当前时间的值-- 时间点2
15          // tv_cache <--- now
16         gettime(base, &base->tv_cache);
17         // .. 处理就绪事件
18     }
19     // 退出时也要清空时间缓存
20     base->tv_cache.tv_sec = 0;
21     return (0);
22 }

 

     时间event_tv指示了dispatch()上次返回,也就是I/O事件就绪时的时间,第一次进入循环时,因为tv_cache被清空,所以gettime()执行系统调用获取当前系统时间;然后将会更新为tv_cache指示的时间。
     时间tv_cache在dispatch()返回后被设置为当前系统时间,所以它缓存了本次I/O事件就绪时的时间(event_tv)。
     从代码逻辑里能够看出event_tv取得的是tv_cache上一次的值,所以event_tv应该小于tv_cache的值。
     设置时间缓存的优势是没必要每次获取时间都执行系统调用,这是个相对费时的操做;在上面标注的时间点2到时间点1的这段时间(处理就绪事件时),调用gettime()取得的都是tv_cache缓存的时间。

 

3, 时间校订

     若是系统支持monotonic时间,该时间是系统从boot后到如今所通过的时间,所以不须要执行校订。
     根据前面的代码逻辑,若是系统不支持monotonic时间,用户可能会手动的调整时间,若是时间被向前调整了(MS前面第7部分讲成了向后调整,要改正),好比从5点调整到了3点,那么在时间点2取得的值可能        会小于上次的时间,这就须要调整了,下面来看看校订的具体代码,由函数timeout_correct()完成:

 1 static void timeout_correct(struct event_base *base, struct timeval *tv)
 2 {
 3     struct event **pev;
 4     unsigned int size;
 5     struct timeval off;
 6     if (use_monotonic) // monotonic时间就直接返回,无需调整
 7         return;
 8     gettime(base, tv); // tv <---tv_cache
 9     // 根据前面的分析能够知道event_tv应该小于tv_cache
10     // 若是tv < event_tv代表用户向前调整时间了,须要校订时间
11     if (evutil_timercmp(tv, &base->event_tv, >=)) {
12         base->event_tv = *tv;
13         return;
14     }
15     // 计算时间差值
16     evutil_timersub(&base->event_tv, tv, &off);
17     // 调整定时事件小根堆
18     pev = base->timeheap.p;
19     size = base->timeheap.n;
20     for (; size-- > 0; ++pev) {
21         struct timeval *ev_tv = &(**pev).ev_timeout;
22         evutil_timersub(ev_tv, &off, ev_tv);
23     }
24     base->event_tv = *tv; // 更新event_tv为tv_cache
25 }

 

在调整小根堆时,由于全部定时事件的时间值都会被减去相同的值,所以虽然堆中元素的时间键值改变了,可是相对关系并无改变,不会改变堆的总体结构。所以只须要遍历堆中的全部元素,将每一个元素的时间键值减去相同的值便可完成调整,不须要从新调整堆的结构。
固然调整完后,要将event_tv值从新设置为tv_cache值了。

 

4, 小节

主要分析了一下libevent对系统时间的处理,时间缓存、时间校订和定时堆的时间值调整等,逻辑仍是很简单的,时间的加减、设置等辅助函数则很是简单,主要在头文件evutil.h中,就再也不多说了

 

                                       第十二章    让libevent支持多线程

 

Libevent自己不是多线程安全的,在多核的时代,如何能充分利用CPU的能力呢,这一节来讲说如何在多线程环境中使用libevent,跟源代码并无太大的关系,纯粹是使用上的技巧。

1, 错误使用示例

在多核的CPU上只使用一个线程始终是对不起CPU的处理能力啊,那好吧,那就多建立几个线程,好比下面的简单服务器场景

     1> 主线程建立工做线程1;
     2> 接着主线程监听在端口上,等待新的链接;
     3> 在线程1中执行event事件循环,等待事件到来;
     4> 新链接到来,主线程调用libevent接口event_add将新链接注册到libevent上;

 

上面的逻辑看起来没什么错误,在不少服务器设计中均可能用到主线程和工做线程的模式….
但是就在线程1注册事件时,主线程极可能也在操做事件,好比删除,修改,经过libevent的源代码也能看到,没有同步保护机制,问题麻烦了,看起来不能这样作啊,难道只能使用单线程不成!?

 

2, 支持多线程的几种模式

Libevent并非线程安全的,但这不表明libevent不支持多线程模式,其实方法在前面已经将signal事件处理时就接触到了,那就是消息通知机制。
一句话,“你发消息通知我,而后再由我在合适的时间来处理”;
说到这就再多说几句,再打个比方,把你本身比做一个工做线程,而你的头是主线程,你有一个消息信箱来接收别人发给你的消息,当时头有个新任务要指派给你。

 

2.1 暴力抢占

那么第一节中使用的多线程方法至关下面的流程:
     1> 当时你正在作事,好比在写文档;
     2> 你的头找到了一个任务,要指派给你,好比帮他搞个PPT,哈;
     3> 头命令你立刻搞PPT,你这是不得不中止手头的工做,把PPT搞定了再接着写文档;

 

2.2 纯粹的消息通知机制

那么基于纯粹的消息通知机制的多线程方式就像下面这样:
     1> 当时你正在写文档;
     2> 你的头找到了一个任务,要指派给你,帮他搞个PPT;
     3> 头发个消息到你信箱,有个PPT要帮他搞定,这时你并不鸟他;
     4> 你写好文档,接着检查消息发现头有个PPT要你搞定,你开始搞PPT;

 

第一种的好处是消息能够当即获得处理,可是很方法很粗暴,你必须当即处理这个消息,因此你必须处理好切换问题,免得把文档上的内容不当心写到PPT里。在操做系统的进程通讯中,消息队列(消息信箱)都是操做系统维护的,你没必要关心。
第二种的优势是经过消息通知,切换问题省心了,不过消息是不能当即处理的(基于消息通知机制,这个老是不免的),并且全部的内容都经过消息发送,好比PPT的格式、内容等等信息,这无疑增长了通讯开销。

 

2.3 消息通知+同步层

有个折中机制能够减小消息通讯的开销,就是提取一个同步层,还拿上面的例子来讲,你把工做安排都存放在一个工做队列中,并且你可以保证“任何人把新任务扔到这个队列”,“本身取出当前第一个任务”等这些操做都可以保证不会把队列搞乱(其实就是个加锁的队列容器)。
再来看看处理过程和上面有什么不一样:
     1> 当时你正在写文档;
     2> 你的头找到了一个任务,要指派给你,帮他搞个PPT;
     3> 头有个PPT要你搞定,他把任务push到你的工做队列中,包括了PPT的格式、内容等信息;
     4> 头发个消息(一个字节)到你信箱,有个PPT要帮他搞定,这时你并不鸟他;
     5> 你写好文档,发现有新消息(这预示着有新任务来了),检查工做队列知道头有个PPT要你搞定,你开始搞PPT;

工做队列其实就是一个加锁的容器(队列、链表等等),这个很容易实现实现;而消息通知仅须要一个字节,具体的任务都push到了在工做队列中,所以想比2.2减小了很多通讯开销。
多线程编程有不少陷阱,线程间资源的同步互斥不是一两句能说得清的,并且出现bug很难跟踪调试;这也有不少的经验和教训,所以若是让我选择,在绝大多数状况下都会选择机制3做为实现多线程的方法。

 

3 , 例子——memcached

Memcached中的网络部分就是基于libevent完成的,其中的多线程模型就是典型的消息通知+同步层机制。下面的图足够说明其多线程模型了,其中有详细的文字说明。

 

4, 小节

本节更是libevent的使用方面的技巧,讨论了一下如何让libevent支持多线程,以及几种支持多线程的机制,和memcached使用libevent的多线程模型

 

                                       第十三章  libevent 信号处理注意点

 

前面讲到了 libevent 实现多线程的方法,然而在多线程的环境中注册信号事件,仍是有一些状况须要当心处理,那就是不能在多个 libevent 实例上注册信号事件。依然冠名追加到 libevent 系列。

以 2 个线程为例,作简单的场景分析。

 

 

1> 首先是建立并初始化线程 1 的 libevent 实例 base1 ,线程 1 的 libevent 实例 base2 ;

2 >在 base1 上注册 SIGALRM 信号;在 base2 上注册 SIGINT 信号;

3 >假设当前 base1 和 base2 上都没有注册其余的事件;

4 >线程 1 和 2 都进入 event_base_loop 事件循环:

 1 event_base_loop(base1)                                      event_base_loop(base2)
 2 
 3 {                                                                            {
 4 
 5 if (base2->sig.ev_signal_added)                          if (base2->sig.ev_signal_added)
 6 
 7 evsignal_base = base1;                                     evsignal_base = base2;
 8 
 9 while(!done)                                                         while(!done)
10 
11 {                                                                            {
12 
13      …                                                                              …
14 
15      evsel->dispatch(…);                                               evsel->dispatch(…);
16 
17      …                                                                             …
18 
19 }                                                                            }
20 
21 }                                                                             }

 

5> 假设线程 1 先进入 event_base_loop ,并设置 evsignal_base = base1 ;并等待;

6> 接着线程 2 也进入 event_base_loop ,并设置 evsignal_base = base2 ;并等待;

  因而 evsignal_base 就指向了 base2 ;

7> 信号 ALARM 触发,调用服务例程:

 1 static void evsignal_handler(int sig)
 2 
 3 {
 4 
 5        ...
 6 
 7        evsignal_base->sig.evsigcaught[sig]++;
 8 
 9        evsignal_base->sig.evsignal_caught = 1;
10 
11        /* Wake up our notification mechanism */
12 
13        send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
14 
15        ...
16 
17 }

 

因而 base2 获得通知 ALARM 信号发生了,而实际上 ALARM 是注册在 base1 上的, base2 上的 ALARM 注册 event 是空的,因而处理函数将不能获得调用;

所以在 libevent 中,若是须要处理信号,只能将信号注册到一个 libevent 实例上。

memcached 就没有使用 libevent 提供的 signal 接口,而是直接使用系统提供的原生 API ,看起来这样更简洁。