libev源码解析——总览

2017-08-10 18:36:56 新飞舞

不知道是被墙了还是网站不再维护,它的官网(http://libev.schmorp.de/)在国内已经没法访问了。但是我们仍然可以从github上下载其源码(https://github.com/enki/libev)。

使用样例

libev支持相对时间定时器、绝对时间定时器、文件状态监控和信号监控等功能。我们可以在它基础上,通过少量的代码实现稳健完善的功能。

我们先看一段实现定时器功能的代码

#include <ev.h>
#include <stdio.h>

ev_timer timeout_watcher;

static void
timeout_cb(EV_P_ ev_timer *w, int revents)
{
    puts("timeout");
    ev_break(EV_A_ EVBREAK_ONE);
}

int main(void)
{
    struct ev_loop *loop = EV_DEFAULT;
    ev_timer_init(&timeout_watcher, timeout_cb, 5.5, 0);
    ev_timer_start(loop, &timeout_watcher);
    ev_run(loop, 0);
    return 0;
}

这段代码的结构非常简单。首先我们要定义一个名为timeout_cb的回调函数用于响应定时器。然后定义一个ev_timer结构(监视器),它通过ev_timer_init进行初始化。初始化的参数包含之前定义的响应函数指针和迭代器超时时间。ev_timer准备好后,通过ev_timer_start将其和一个ev_loop进行绑定。最后使用ev_run方法运行起来这个ev_loop指针,从而实现一个完整的定时器功能。

可见使用libev库非常方便。其实我们之后见到的其他用法和上面步骤是类似的,即:

  • 初始化ev_loop。
  • 定义监视器。
  • 定义回调函数。
  • 监视器和回调函数关联。
  • 监视器和ev_loop关联。
  • ev_run将ev_loop运行起来。

假如上面代码是个框架使用的雏形,那么如果让我们去设计这样的框架,该如何设计呢?

模型设计

首先我们需要考虑到的是使用sleep还是使用事件模型去实现逻辑等待。

如果使用sleep方法,那么我们就要在唤醒后去检测各个事件,比如要检测文件状态是否发生变化,比如定时器时间是否已经超时。于是有个问题,就是sleep多久怎么确定?我们不知道是5秒后还是1秒后文件状态发生变化,那么只能最小粒度sleep了。那么这就意味着线程在短暂挂起后,马上检测一系列可能尚未发生改变的事件。这种设计明显很消耗CPU,而且非常低效。

如果使用事件模型去等待,就可以解决上述问题。但是像定时器,在系统中并没有事件与其对应。于是我们需要考虑下对于没有事件对应的功能怎么通过事件模型去封装。

其次我们需要考虑使用单线程模型还是多线程模型。

单线程模型是让主流程和事件响应函数在一个线程中执行。其伪代码是

If (event is ready) {
	event_callback(); // in the main thead
}

其特点是实现简单,但是事件响应函数的效率将严重影响主流程对事件的响应速度。比如A、B两个事件同时发生,理论上我们希望两个事件的响应函数被同时执行,或者在允许存在的系统调用时间差(比如创建线程的消耗)内执行。然而单线程模型则会让一个响应函数执行完后再去执行另一响应函数,于是就出现排队现象。所以单线程模型无法保证及时响应。

多线程模型则完全避免了上述问题。它可在事件发生后启动一个线程去处理响应函数。当然相对来说多线程模型比较复杂,需要考虑多线程同步问题。

If (event is ready) {
	thread_excute(event_callback); // run in another thread
}

那么libev对上面两个问题是怎么选择的呢?对于sleep和事件模型,libev选择的是后者,所以它是“高性能”的。对于单线程和多线程,libev选择的是前者。至于原因我并不知道,可能是作者希望它足够简单,或者希望它能在不支持多线程的系统上使用。但是要说明一点,并不是说libev不支持多线程。因为一个单线程模型的执行体,是可以放在其他若干个线程中执行的,只要保证数据同步。

单/多线程编译

libev提供了各种编译选项以支持各种特性。比如在支持多线程的系统上,我们可以指定EV_MULTIPLICITY参数,以让libev编译出多线程版本。

libev对于单线程版本的数据都是以全局静态变量形式提供。而对于多线程版本,则提供了一个结构体——ev_loop保存数据,这样不同线程持有各自的数据对象,从而做到数据隔离。

#if EV_MULTIPLICITY

  struct ev_loop
  {
    ev_tstamp ev_rt_now;
    #define ev_rt_now ((loop)->ev_rt_now)
    #define VAR(name,decl) decl;
      #include "ev_vars.h"
    #undef VAR
  };
  #include "ev_wrap.h"

  static struct ev_loop default_loop_struct;
  EV_API_DECL struct ev_loop *ev_default_loop_ptr = 0; /* needs to be initialised to make it a definition despite extern */

#else

  EV_API_DECL ev_tstamp ev_rt_now = 0; /* needs to be initialised to make it a definition despite extern */
  #define VAR(name,decl) static decl;
    #include "ev_vars.h"
  #undef VAR

  static int ev_default_loop_ptr;

#endif

不管是哪个版本,它们都提供了ev_default_loop_ptr变量。多线程版本它将指向全局静态变量default_loop_struct,这样对于使用了多线程版本又不想维护ev_loop结构对象的用户来说,直接使用这个对象就行了,非常方便。

然后再看下ev_vars.h的引入。其实现如下:

#define VARx(type,name) VAR(name, type name)

VARx(ev_tstamp, now_floor) /* last time we refreshed rt_time */
VARx(ev_tstamp, mn_now)    /* monotonic clock "now" */
VARx(ev_tstamp, rtmn_diff) /* difference realtime - monotonic time */

/* for reverse feeding of events */
VARx(W *, rfeeds)
VARx(int, rfeedmax)
VARx(int, rfeedcnt)

VAR (pendings, ANPENDING *pendings [NUMPRI])
VAR (pendingmax, int pendingmax [NUMPRI])
VAR (pendingcnt, int pendingcnt [NUMPRI])
……

在多线程版本中,它在ev_loop结构体中被引入的。这样在编译器展开文件时,它将会被定义到结构体内部。在单线程版本中,VAR宏被声明为定义一个静态全局变量的形式。这种利用宏和编译展开技术,在不同结构中定义相同类型数据的方式还是很有意思的。

但是又会有个问题,如何去访问这些变量呢?在单线程中,它们是静态变量,所有位置可以直接通过名称访问。而多线程版本中,则需要通过一个ev_loop结构体去引导。相关的代码总不能通过EV_MULTIPLICITY宏来区分不同变量形式吧?如果那样,代码将变得非常难看。我们看下libev怎么巧妙解决这个问题的。

上面代码块的多线程定义区间,引入了ev_wrap.h文件。其实现如下:

#ifndef EV_WRAP_H
#define EV_WRAP_H
#define acquire_cb ((loop)->acquire_cb)
#define activecnt ((loop)->activecnt)
#define anfdmax ((loop)->anfdmax)
#define anfds ((loop)->anfds)
#define async_pending ((loop)->async_pending)
#define asynccnt ((loop)->asynccnt)
……

这样使用一个和变量相同名称的宏替代了通过ev_loop结构体对象访问的变量。且这个宏名称和单线程版本中静态变量名相同。这样就让不同版本的关键变量“同名”了。于是代码对这些变量的访问直接使用其原始名称即可——单线程中使用的是真实变量名,多线程中使用的是宏。

这样的设计,又引入一个问题。那就是所有使用这些变量的函数,在多线程版本中,需要提供一个名字为loop的ev_loop结构体对象;而在单线程版本中则不需要。为了固化这个名称,libev还为此专门定义了一系列宏。

#if EV_MULTIPLICITY
struct ev_loop;
# define EV_P  struct ev_loop *loop               /* a loop as sole parameter in a declaration */
# define EV_P_ EV_P,                              /* a loop as first of multiple parameters */
# define EV_A  loop                               /* a loop as sole argument to a function call */
# define EV_A_ EV_A,                              /* a loop as first of multiple arguments */
# define EV_DEFAULT_UC  ev_default_loop_uc_ ()    /* the default loop, if initialised, as sole arg */
# define EV_DEFAULT_UC_ EV_DEFAULT_UC,            /* the default loop as first of multiple arguments */
# define EV_DEFAULT  ev_default_loop (0)          /* the default loop as sole arg */
# define EV_DEFAULT_ EV_DEFAULT,                  /* the default loop as first of multiple arguments */
#else
# define EV_P void
# define EV_P_
# define EV_A
# define EV_A_
# define EV_DEFAULT
# define EV_DEFAULT_
# define EV_DEFAULT_UC
# define EV_DEFAULT_UC_
# undef EV_EMBED_ENABLE
#endif

之后我们在代码中导出可见的EV_P和EV_A就是为了保证不同版本的实现在代码层面是相同的。

相关帖子
用户评论
开源开发学习小组列表