LibEvent Source Code

 这两天没事,看了一下Memcached和libevent的源码,做个小总结。

 

1、入门

1.1、概述
Libevent是一个用于开发可扩展性网络服务器的基于事件驱动(event-driven)模型的网络库。Libevent有几个显著的亮点:
(1)事件驱动(event-driven),高性能;
(2)轻量级,专注于网络,不如 ACE 那么臃肿庞大;
(3)源代码相当精炼、易读;
(4)跨平台,支持 Windows、Linux、*BSD和 Mac Os;
(5)支持多种 I/O多路复用技术, epoll、poll、dev/poll、select 和kqueue 等;
(6)支持 I/O,定时器和信号等事件;
(7)注册事件优先级;
 Libevent 已经被广泛的应用,作为底层的网络库;比如 memcached、 Vomi t、 Nylon、 Netchat等等。

 

1.2、一个简单示例

代码
复制代码
 1  int  lasttime;
 2 
 3  static   void
 4  timeout_cb( int  fd,  short   event void   * arg)
 5  {
 6  struct  timeval tv;
 7  struct   event   * timeout  =  arg;
 8  int  newtime  =  time(NULL);
 9 
10  // printf("%s: called at %d: %d\n", __func__, newtime,
11  printf( " %s: called at %d: %d\n " " timeout_cb " , newtime,
12          newtime  -  lasttime);
13  lasttime  =  newtime;
14 
15  evutil_timerclear( & tv);
16  tv.tv_sec  =   2 ;
17  // 重新注册event
18  event_add(timeout,  & tv);
19  }
20 
21  int
22  main ( int  argc,  char   ** argv)
23  {
24  struct   event  timeout;
25  struct  timeval tv;
26   
27  /*  Initalize the event library  */
28  // 初始化event环境
29  event_init();
30 
31  /*  Initalize one event  */
32  // 设置事件
33  evtimer_set( & timeout, timeout_cb,  & timeout);
34 
35  evutil_timerclear( & tv);
36  tv.tv_sec  =   2 ;
37  // 注册事件
38  event_add( & timeout,  & tv);
39 
40  lasttime  =  time(NULL);
41      
42  // 等待,分发,处理事件
43  event_dispatch();
44 
45  return  ( 0 );
46  }
复制代码

这是一个简单的基于libevent的定时器程序,运行结果:

 

用libevent编程非常简单,只需要调用event_init初始化环境,然后调用event_add注册相应的事件,接着调用event_dispatch等待并处理相应的事件即可。
调用event_add注册事件时,设置其回调函数。Libevent检测到事件发生时,便会调用事件对应的回调用函数,执行相关的业务逻辑。

1.3、源代码结构
Libevent 的源代码虽然都在一层文件夹下面,但是其代码分类还是相当清晰的,主要可分为头文件、内部使用的头文件、辅助功能函数、日志、libevent 框架、对系统 I/O 多路复用机制的封装、信号管理、定时事件管理、缓冲区管理、基本数据结构和基于 libevent的两个实用库等几个部分,有些部分可能就是一个源文件。
(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 查询库;

2、核心对象
结构体event和event_base是libevent的两个核心数据结构,前者代表一个事件对象,后者代表整个事件处理框架。
2.1、event(事件)

复制代码
代码
 1  // event.h
 2  struct   event  {
 3  TAILQ_ENTRY ( event ) ev_next;           // 已注册事件链表
 4  TAILQ_ENTRY ( event ) ev_active_next; // 就绪事件链表
 5  TAILQ_ENTRY ( event ) ev_signal_next;  // signal链表
 6  unsigned  int  min_heap_idx;     /*  for managing timeouts,事件在堆中的下标  */
 7 
 8  struct  event_base  * ev_base;
 9 
10  int  ev_fd;       // 对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号
11  short  ev_events;  // event关注的事件类型
12  short  ev_ncalls;  // 事件就绪执行时,调用 ev_callback 的次数
13  short   * ev_pncalls;     /*  Allows deletes in callback  */
14 
15  struct  timeval ev_timeout;   // timout事件的超时值
16 
17  int  ev_pri;    /*  smaller numbers are higher priority,优先级  */
18 
19  void  ( * ev_callback)( int short void   * arg);  // 回调函数
20  void   * ev_arg;  // 回调函数的参数
21 
22  int  ev_res;         /*  result passed to event callback  */
23  int  ev_flags;  // event的状态
24  };
25 
复制代码

Libevent通过event对象将I/O事件、信号事件和定时器事件封装,从而统一处理,这也是libevent的精妙所有。
各个字段的具体含义:
(1) ev_events:event关注的事件类型,它可以是以下3种类型:
I/O事件: EV_WRITE和EV_READ
定时事件:EV_TIMEOUT
信号:    EV_SIGNAL
辅助选项:EV_PERSIST,表明是一个永久事件
libevent中的定义为:
#define EV_TIMEOUT    0x01
#define EV_READ    0x02
#define EV_WRITE    0x04
#define EV_SIGNAL    0x08
#define EV_PERSIST    0x10    /* Persistant event */
(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指向事件框架实例。
(5)ev_fd,对于 I/O事件,是绑定的文件描述符;对于 signal 事件,是事件对应的信号;
(6)eb_flags:libevent 用于标记 event信息的字段,表明事件当前的状态,可能的值有:
#define EVLIST_TIMEOUT   0x01 // event在time堆中
#define EVLIST_INSERTED 0x02 // event在已注册事件链表中
#define EVLIST_SIGNAL    0x04 // 未见使用
#define EVLIST_ACTIVE    0x08 // event在激活链表中
#define EVLIST_INTERNAL 0x10 // 内部使用标记
#define EVLIST_INIT      0x80 // event 已被初始化

2.2、event_base(事件处理框架)

 

复制代码
代码
 1  // evenet_internal.h
 2  struct  event_base {
 3  const   struct  eventop  * evsel;  // 底层具体I/O demultiplex操作函数集
 4  void   * evbase;
 5  int  event_count;         /*  counts number of total events,总的事件数量  */
 6  int  event_count_active;     /*  counts number of active events,就绪事件数量  */
 7 
 8  int  event_gotterm;         /*  Set to terminate loop  */
 9  int  event_break;         /*  Set to terminate loop immediately  */
10 
11  /*  active event management  */
12  // 就绪事件链表数组
13  struct  event_list  ** activequeues;
14  int  nactivequeues; // 就绪事件队列个数
15 
16  /*  signal handling info  */
17  struct  evsignal_info sig;  // 用于管理信号
18 
19  struct  event_list eventqueue;  // 注册事件队列
20  struct  timeval event_tv;
21 
22  struct  min_heap timeheap;  // 管理定时器的小根堆
23  struct  timeval tv_cache;  // 记录时间缓存
24  };
复制代码

(1)evsel:libevent支持Linux、Windows等多种平台,也支持epoll、poll、select、kqueue等多种I/O多路复用模型。如果把event_init、event_add看成高层抽象的统一事件操作接口,则evsel为这些函数在底层具体的I/O demultiplex的对应的操作函数集。eventop为函数指针的集合:

复制代码
代码
 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  };
11 
复制代码

在初始化函数event_base_new中,libevent将evsel指向全局数组eventops的具体元素:

代码


2.3、主要函数
2.3.1、event_int(初始化libevent实例)
struct event_base *
event_init(void)
初始化事件处理框架实例,内部调用event_base_new。

event_base_new的主要逻辑:

复制代码
代码
 1  struct  event_base  *
 2  event_base_new( void )
 3  {
 4 
 5  // 初始化小根堆
 6  min_heap_ctor( & base -> timeheap);
 7 
 8  // 初始化注册事件队列
 9  TAILQ_INIT( & base -> eventqueue);
10 
11  for  (i  =   0 ; eventops[i]  &&   ! base -> evbase; i ++ ) {
12  // I/O demultiplex机制实例
13  base -> evsel  =  eventops[i];
14 
15  // 初始化I/O demultiplex实例(参见win32_init)
16  base -> evbase  =   base -> evsel -> init( base );
17  }
18 
19  // 分配1个就绪事件队列
20  event_base_priority_init( base 1 );
21 
22  }
复制代码

2.3.2、event_add(注册事件)
//注册事件
int
event_add(struct event *ev, const struct timeval *tv)
该函数主要将事件ev加入到事件框架event_base的注册事件链表base->eventqueue。

2.3.3、event_del(删除事件)
//删除事件
int
event_del(struct event *ev)
该函数主要将事件ev从相应的链表上删除。

2.3.4、event_set(设置事件)

复制代码
代码
/* 设置event对象
**ev:事件对象
**fd:事件对应的文件描述符或信号,对于定时器设为-1
**events:事件类型,比如 EV_READ,EV_PERSIST, EV_WRITE, EV_SIGNAL
**callback:事件的回调函数
**arg:回调函数参数
*/
void
event_set(
struct   event   * ev,  int  fd,  short  events,
      
void  ( * callback)( int short void   * ),  void   * arg)
复制代码

在将事件注册事件处理框架之前,应该先调用event_set对事件进行相关设置。


2.4、libevent对event的管理
event结构有3个链表结点域和一个小根堆索引,libevent通过3个链表和一个小根堆对I/O事件、signal事件和timer事件进行管理。
对于I/O事件,通过event_add将其加入event_base的注册事件链表eventqueue ;就绪时会加入event_base的就绪链表activequeues[];
对于timer事件,event_add将其加入到event_base的小根堆timeheap;
Signale事件的管理相对复杂些,event_add将其加入到注册事件链表,同时,event_add内部会调用I/O demultiplex的add函数(对于I/O事件也一样),比如epoll_add。而add函数又会调用evsignal_add将其加入到evsignal_info的evsigevents[signo]链表(关于signal,后面会详细介绍)。


3、事件处理框架主循环
Libevent将I/O事件、signal事件和timer事件用统一的模型进行处理,这是非常精妙的。libevent主循环函数不断检测注册事件,如果有事件发生,则将其放入就绪链表,并调用事件的回调函数,完成业务逻辑处理。
3.1、event_dispatch
//事件处理主循环
int
event_dispatch(void)
这是呈现给外部的接口,它的实现很简单,即调用event_loop,而event_loop调用event_base_loop,event_base_loop完成实际的主循环逻辑。

3.2、event_base_loop
主要算法:

代码


3.3、timeout_next

代码


3.4、dispatch函数
调用底层I/O multiplex的dispatch函数,具体的实现可以参见epoll的实现epoll_dispatch。

3.5、event_process_active

代码
复制代码
 1  /* 处理就绪事件.
 2  **就绪事件位于优先级队列中,低优先级通常比高优先级队列先处理,所以,
 3  **高优先级队列可能饿死.
 4  */
 5  static   void
 6  event_process_active( struct  event_base  * base )
 7  {
 8       struct   event   * ev;
 9       struct  event_list  * activeq  =  NULL;
10       int  i;
11       short  ncalls;
12 
13       for  (i  =   0 ; i  <   base -> nactivequeues;  ++ i) {
14           if  (TAILQ_FIRST( base -> activequeues[i])  !=  NULL) {
15               // 一次只处理一个就绪事件链表
16              activeq  =   base -> activequeues[i];
17               break ;
18          }
19      }
20 
21      assert(activeq  !=  NULL);
22 
23       // 处理就绪事件链表上的所有事件
24       for  (ev  =  TAILQ_FIRST(activeq); ev; ev  =  TAILQ_FIRST(activeq)) {
25          
26           // 先将事件从链表上删除
27           if  (ev -> ev_events  &  EV_PERSIST)
28              event_queue_remove( base , ev, EVLIST_ACTIVE);
29           else
30              event_del(ev);  // 删除事件
31          
32           /*  Allows deletes to work  */
33          ncalls  =  ev -> ev_ncalls;
34          ev -> ev_pncalls  =   & ncalls;
35           while  (ncalls) {
36              ncalls -- // 调用次数减1
37              ev -> ev_ncalls  =  ncalls;
38               // 调用事件的回调函数
39              ( * ev -> ev_callback)(( int )ev -> ev_fd, ev -> ev_res, ev -> ev_arg);
40               if  ( base -> event_break)
41                   return ;
42          }
43      }
44  }
45 
复制代码


4、Timer事件
Timer事件的处理本身比较简单,不再赘述。

5、signal事件
5.1、socket pair
Libevent通过socketpair,将signal事件与I/O事件完美的统一起来。Socketpair,简单的说就一对socket,一端用于写,一端用于读。工作方式如下:

 为了与I/O事件统一起来,libevent内部使用了一个针对read socket的读事件。

5.1.1、Socketpair的创建
与信号事件的初始化工作都是在evsignal_init中完成的,而evsignal_init通过调用evutil_socketpair创建socketpair。对于Unix平台,有socketpair系统调用;对于Windows,则相对复杂一些,具体见evutil_socketpair函数的实现。

5.2、evsignal_info
在event_base内部有一个evsignal_info类型的字段sig,它是用于管理signal事件的核心数据结构:

复制代码
代码
 1  // evsignal.h
 2  struct  evsignal_info {
 3  struct   event  ev_signal;     // 内部socket读事件
 4  int  ev_signal_pair[ 2 ];   // 对应socket pair的两个socket描述符
 5  int  ev_signal_added;   // 内部socket读事件是否已经加入注册链表
 6  volatile  sig_atomic_t evsignal_caught;  // 是否有信号发生
 7  // 信号事件链表数组,evsigevents[signo]表示注册信号signo的事件
 8  struct  event_list evsigevents[NSIG]; 
 9  // 具体记录每个信号触发的次数,evsigcaught[signo]是记录信号 signo被触发的次数
10  sig_atomic_t evsigcaught[NSIG];
11      
12  // sh_old记录了原来的 signal 处理函数指针,当信号 signo 注册的 event 被清空时,需要重新设置其处理函数
13  #ifdef HAVE_SIGACTION
14  struct  sigaction  ** sh_old;
15  #else
16  ev_sighandler_t  ** sh_old;
17  #endif
18  int  sh_old_max;
19  };
20 
复制代码

5.3、主要函数
5.3.1、evsignal_init
主要完成evsignal_info的初始化,主要算法:

复制代码
代码
 1  int
 2  evsignal_init( struct  event_base  * base )
 3  {
 4  evutil_socketpair(AF_UNIX, SOCK_STREAM,  0 base -> sig.ev_signal_pair);
 5  base -> sig.sh_old  =  NULL;
 6  base -> sig.sh_old_max  =   0 ;
 7 
 8  // 事件发生次数设为0
 9  base -> sig.evsignal_caught  =   0 ;
10  memset( & base -> sig.evsigcaught,  0 sizeof (sig_atomic_t) * NSIG);
11  /*  initialize the queues for all events  */
12  for  (i  =   0 ; i  <  NSIG;  ++ i)
13  TAILQ_INIT( & base -> sig.evsigevents[i]);
14 
15  evutil_make_socket_nonblocking( base -> sig.ev_signal_pair[ 0 ]);  // 写端
16 
17  // 设置内部读事件
18  event_set( & base -> sig.ev_signal,  base -> sig.ev_signal_pair[ 1 ], 
19  EV_READ  |  EV_PERSIST, evsignal_cb,  & base -> sig.ev_signal);  // 读端
20  base -> sig.ev_signal.ev_base  =   base ;
21 
22  // sig.ev_signal == EV_READ | EV_PERSIST | EVLIST_INTERNAL
23  base -> sig.ev_signal.ev_flags  |=  EVLIST_INTERNAL;
24  }
复制代码

该函数的关键在于这里会设置libevent用于管理信号事件的内部读事件evsignal_info的ev_signal,并将该事件对应的文件描述符设为socket pair的读端。该函数由I/O multiplex的init函数调用。注:这里只是设置,而并没有注册socket pair的读事件(见下一节)。

5.3.2、evsignal_add
当调用event_add注册信号事件时,内部会先调用I/O multiplex的add函数,add函数又会调用evsignal_add,将事件加到evsignal_info内部的信号事件链表。然后再event_queue_insert将其添加到event_base的注册事件链表。

代码

这里有两个地方需要注意,一是调用_evsignal_set_handler设置外部注册信号事件对应的信号的信号处理函数evsignal_handler:

复制代码
代码
 1  static   void
 2  evsignal_handler( int  sig)
 3  {
 4       int  save_errno  =  errno;
 5 
 6       // 设置信号事件的发生次数
 7      evsignal_base -> sig.evsigcaught[sig] ++ ;
 8      evsignal_base -> sig.evsignal_caught  =   1 ;
 9 
10  #ifndef HAVE_SIGACTION
11      signal(sig, evsignal_handler);
12  #endif
13 
14       /*  Wake up our notification mechanism  */
15       // 向socket pair的写端写数据
16      send(evsignal_base -> sig.ev_signal_pair[ 0 ],  " a " 1 0 );
17      errno  =  save_errno;
18  }
复制代码

当用户注册信号事件对应的信号发生时,OS转到evsignal_handler函数,从而设置sig.evsignal_caught,并向socket pair的写端发送数据。
二是通过调用event_add完成内部socket pair的读事件sig->ev_signal的注册。最后,将(外部)事件添加到信号事件链表。
5.3.2、与主循环结合
信号事件完成了注册,libevent就会在主循环中,等待事件发生,并处理事件。为了理解,来看看具体I/O demultiplex的dispatch函数:

复制代码
代码
 1  static   int
 2  epoll_dispatch( struct  event_base  * base void   * arg,  struct  timeval  * tv)
 3  {
 4       struct  epollop  * epollop  =  arg;
 5       struct  epoll_event  * events  =  epollop -> events;
 6       struct  evepoll  * evep;
 7       int  i, res, timeout  =   - 1 ;
 8 
 9       if  (tv  !=  NULL)
10          timeout  =  tv -> tv_sec  *   1000   +  (tv -> tv_usec  +   999 /   1000 ;
11 
12       if  (timeout  >  MAX_EPOLL_TIMEOUT_MSEC) {
13           /*  Linux kernels can wait forever if the timeout is too big;
14           * see comment on MAX_EPOLL_TIMEOUT_MSEC.  */
15          timeout  =  MAX_EPOLL_TIMEOUT_MSEC;
16      }
17 
18       // 等待I/O事件
19      res  =  epoll_wait(epollop -> epfd, events, epollop -> nevents, timeout);
20 
21       if  (res  ==   - 1 ) {
22           if  (errno  !=  EINTR) {
23              event_warn( " epoll_wait " );
24               return  ( - 1 );
25          }
26           // epoll_wait被信号中断
27          evsignal_process( base );
28           return  ( 0 );
29      }  else   if  ( base -> sig.evsignal_caught) { // 发生了信号事件
30           // 处理信号事件
31          evsignal_process( base );
32      }
33  //
34  }
复制代码

epoll_dispatch函数调用epoll_wait函数等待I/O发生。然后,如果有信号事件发生,则调用evsignal_process处理信号事件,evsignal_process的逻辑比较简单,它只是将事件从注册事件链表转移到就绪事件链表。

还记得evsignal_handler函数吗?它是所有(外部)信号事件对应的信号的信号处理函数,将实际的信号发生时,OS会转而执行evsignal_handler函数,而它便向socket pair的写端写数据,而读端收到数据。而此时,libevent的内部socket pair读事件已经完成注册。libevent正阻塞在epoll_wait处,当socketp pair读端收到数据时,libevent便从epoll_wait处返回。总之,signal事件通过socket pair,与I/O事件实现完美的统一。

Libevent从epoll_wait返回后,它调用evsignal_process处理信号事件,然后调用event_active将I/O事件(包括内部的socket pair读事件)转移到就绪事件链表。

Socket pair的读事件回调函数:

复制代码
代码
 1  static   void
 2  evsignal_cb( int  fd,  short  what,  void   * arg)
 3  {
 4  static   char  signals[ 1 ];
 5  #ifdef WIN32
 6  SSIZE_T n;
 7  #else
 8  ssize_t n;
 9  #endif
10  // 接收数据
11  =  recv(fd, signals,  sizeof (signals),  0 );
12  if  (n  ==   - 1 )
13  event_err( 1 " %s: read " , __func__);
14  }
复制代码


6、libevent的应用
Libevent是一个非常优秀的开源网络库,它被许多其它开源程序使用。Memcache使用libevent作为底层的网络处理组件,并采用主线程(main thread,单一)+工作线程(work thread,多个)的多线程模型(这将在Memcached的分析中详细介绍)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值