libev源码学习

libev源码学习

作者:admin |  时间:2014-08-17 |  浏览:1134 |  0 条评论

先来看一段程序:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <errno.h>
#include <netinet/in.h>
#include <strings.h>
#include "ev.h"

#define PORT 8333
#define BUFFER_SIZE 1024

//gcc test.c -lm ev.o
void accept_cb(struct ev_loop *loop, struct ev_io *watcher, int revents);

static void timeout_cb (EV_P_ ev_timer *w, int revents) ;
void read_cb(struct ev_loop *loop, struct ev_io *watcher, int revents);

int main() {
  struct ev_loop *loop = ev_default_loop(0);
  int sd;
  struct sockaddr_in addr;
  int addr_len = sizeof(addr);
  struct ev_io socket_watcher;
  ev_timer timeout_watcher;

  if( (sd = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
  {
    printf("socket error. errno:%d\n", errno);
    return -1;
  }
  bzero(&addr, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(PORT);
  addr.sin_addr.s_addr = INADDR_ANY;
  if (bind(sd, (struct sockaddr*) &addr, sizeof(addr)) != 0)       {
    printf("bind error.errno:%d\n",errno);
  }
  if (listen(sd, 2) < 0) {
    printf("listen error\n");
    return -1;
  }
  printf("ev_loop beg\n") ;

  //设置cb函数,字段等  
  ev_io_init(&socket_watcher, accept_cb, sd, EV_READ);
  ev_io_start(loop, &socket_watcher);

  ev_timer_init (&timeout_watcher, timeout_cb, 2, 1);
  ev_timer_start (loop, &timeout_watcher);

  while (1) {
    printf("ev_loop\n") ;
    ev_loop(loop, 0);
  }
  return 0;
}

void accept_cb(struct ev_loop *loop, struct ev_io *watcher, int revents){
  struct sockaddr_in client_addr;
  socklen_t client_len = sizeof(client_addr);
  int client_sd;

  struct ev_io *w_client = (struct ev_io*) malloc (sizeof(struct ev_io));
  if(EV_ERROR & revents){
    printf("error event in accept\n");
    return;
  }

  client_sd = accept(watcher->fd, (struct sockaddr *)&client_addr, &client_len);
  if (client_sd < 0) {
    printf("accept error\n");
    return;
  }
  printf("someone connected.\n");

  ev_io_init(w_client, read_cb, client_sd, EV_READ);
  ev_io_start(loop, w_client);
}

void read_cb(struct ev_loop *loop, struct ev_io *watcher, int revents){
  char buffer[BUFFER_SIZE];
  ssize_t read;

  if(EV_ERROR & revents) {
    printf("error event in read");
    return;
  }

  read = recv(watcher->fd, buffer, BUFFER_SIZE, 0);
  if(read < 0){
    printf("read error,errno:%d\n", errno);
    return;
  }
  if(read == 0) {
    printf("someone disconnected.errno:%d\n", errno);
    ev_io_stop(loop,watcher);
    free(watcher);
    return;
  } else {
    printf("get the message:%s\n",buffer);
  }

  send(watcher->fd, buffer, read, 0);
  bzero(buffer, read);
}

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

100行左右的代码,很简单的echo服务,这是libev程序的基本写法,下面分析一下主要的函数。由于现在大部分系统都使用epoll,所以下面假定用的是epoll模式。

一、ev_default_loop初始化epoll句柄

ev_default_loop函数返回一个struct ev_loop *的变量地址,实际上是全局变量default_loop_struct,这个结构体的内容如下,其实就是整个事件循环的总结构体,里面很多成员变量,全部都是用宏定义在ev_vars.h中。

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"

//ev_vars.h
VAR (pendings, ANPENDING *pendings [NUMPRI])  //实际上就是ANPENDING * ev_loop->pendings ,是个指针数组,实际上当优先级队列用的,类似于linux内核里面的进程优先级队列。
VARx(ANFD *, anfds) //同上,这个用来记录每个SOCK的注册的事件列表,用fd索引。

//ev_wrap.h
 #define anfds ((loop)->anfds)   //逆天了,不就直接写一下嘛,用的这么生僻··#define pendings ((loop)->pendings)
比如定义的总之一句,这个结构就是整个库的中心结构,里面包含各种需要的成员变量。

二、事件初始化

接下来程序用socket新建了一个SOCK句柄,并且绑定了本地端口,然后用listen设置句柄为监听状态。 这些都是比较通用的,跟libev没有太大关系。 后面就需要针对不同的句柄,设置不同的事件回调。

值得注意的是libev支持文件通知,定时器,管道等,其分别用ev_io, ev_timer等表示,记录相关的数据,然后分别用不同的函数初始化ev_io_init, ev_timer_init,实际上就是讲几份代码放在一起,稍微柔和下。从而支持不同的类型。

ev_io_init 函数初始化epoll的回调函数,以及记录事件的参数,存放在ev_io结构里面,但此时还没有加入到epoll里面的。

ev_io_init(&socket_watcher, accept_cb, sd, EV_READ);//关注可读事件,回调函数为accept_cb,用来接收新连接。
//函数内容:
#define ev_io_init(ev,cb,fd,events)          do { ev_init ((ev), (cb)); ev_io_set ((ev),(fd),(events)); } while (0)

#define ev_init(ev,cb_) do {            \
  ((ev_watcher *)(void *)(ev))->active  =   \
  ((ev_watcher *)(void *)(ev))->pending = 0;    \
  ev_set_priority ((ev), 0);            \ //设置优先级,注意libev是能支持对不同的事件加优先级的,这个优先级决定:在同时epoll_waite返回的多个句柄后,优先级高的先得到回调。
  ev_set_cb ((ev), cb_);            \    //设置回调函数到ev->cb, 注意这个跟其他的比如redis,nginx会分别有2个函数不一样。
} while (0)

#define ev_io_set(ev,fd_,events_)            do { (ev)->fd = (fd_); (ev)->events = (events_) | EV__IOFDSET; } while (0)

可以看出ev_io_init没有做什么实质性的事情,就是记录了一下几个参数而已。

ev_io_start 函数虽然名字叫start,但实际有点骗人,因为没有start,记录这个句柄的事件,实际上还没有加入epoll的, 只是记录到了fdchanges里面。

libev里面对于所有的SOCKT句柄,都会在ev_loop->anfds数组里面按下标索引了一个槽位,其里面是个anfds[fd].head链表,每个节点代表我们上面用ev_io_init设置的事件,因此如果要同时关注可读,可写事件,需要调用2次ev_io_init,ev_io_start分别设置可读可写事件。

void noinline
ev_io_start (EV_P_ ev_io *w) EV_THROW
{//记录这个句柄的事件,实际上还没有加入epoll的, 只是记录到了fdchanges里面
  int fd = w->fd;

  //检查是否已经开启了
  if (expect_false (ev_is_active (w)))
    return;

  assert (("libev: ev_io_start called with negative fd", fd >= 0));
  assert (("libev: ev_io_start called with illegal event mask", !(w->events & ~(EV__IOFDSET | EV_READ | EV_WRITE))));

  EV_FREQUENT_CHECK;

  ev_start (EV_A_ (W)w, 1);
  array_needsize (ANFD, anfds, anfdmax, fd + 1, array_init_zero);//为新的句柄准备空间,放到(loop)->anfds里面去的
  wlist_add (&anfds[fd].head, (WL)w);//将W插入到anfds[fd]的头部,挂一个未处理事件,比如增加,删除啥的

  /* common bug, apparently */
  assert (("libev: ev_io_start called with corrupted watcher", ((WL)w)->next != (WL)w));

  fd_change (EV_A_ fd, w->events & EV__IOFDSET | EV_ANFD_REIFY);
  w->events &= ~EV__IOFDSET;

  EV_FREQUENT_CHECK;
}
inline_size void
fd_change (EV_P_ int fd, int flags)
{//标记这个句柄的改动到fdchanges,后面run的时候会处理加入epoll的
  unsigned char reify = anfds [fd].reify;
  anfds [fd].reify |= flags;

  if (expect_true (!reify))
    {
      ++fdchangecnt;
      array_needsize (int, fdchanges, fdchangemax, fdchangecnt, EMPTY2);
      fdchanges [fdchangecnt - 1] = fd;//因为这里根本没有将句柄加入epoll, 而是将这个有改动事件的事情记录
      //到ev_loop->fdchanges .意思是说:这个fdchanges数组里面的句柄都有新的事件加入或者删除,待会需要调用epoll_ctl来注册。
    }
}

上面比较关注的函数是,因为这里根本没有将句柄加入epoll, 而是将这个有改动事件的事情记录到ev_loop->fdchanges .意思是说:这个fdchanges数组里面的句柄都有新的事件加入或者删除,待会需要调用epoll_ctl来注册。

对于定时器事件来说,大体类似,只是不需要epoll了。就不多说了。下面看最主要的ev_loop循环。

三、ev_loop事件监听循环

实际上ev_loop是个宏观定义,内容就是ev_run   (EV_A_ flags); ev_run 函数挺大的,分步介绍。

int ev_run (EV_P_ int flags) {
  do {
      fd_reify (EV_A);//这个函数是将ev_io_start开启的事件记录的fdchanges列表设置到epoll句柄监听事件集里面去
      /* calculate blocking time */
      {
         //···根据定时器的最近触发时间,计算这次的epoll等待最长可以等待多久。
      }
        //这里虽然wait了,而且解析读写事件类型了,但是还没有调用那些处理函数的,只是将他们挂入了pendings 的优先级队列
        backend_poll (EV_A_ waittime);//实际上调用的是epoll_wait等待监听事件可读可写
        time_update (EV_A_ waittime + sleeptime);
        timers_reify (EV_A); /* relative timers called last */

        EV_INVOKE_PENDING;// FUCK, 又用宏,实际上就是调用了ev_invoke_pending函数,一个个将上面backend_poll里面
        //检测到的有事件的SOCKT pendings队列里面的事件进行处理
        ...
   }while(...)
}
  • ev_run简化一下就是这么简单,先用fd_reify将之前调用ev_io_init,ev_io_start挂到ev_loop->fdchanges数组里面的socket句柄一个个处理掉,也就是放入epoll监听事件集合中。
  • 然后调用backend_poll进行等待监听可读写事件,实际上是调用epoll_poll 函数,后者再调用epoll_wait真正去等待事件。
  • 最后用宏EV_INVOKE_PENDING 来将发生过事情的事件回调函数按优先级触发。

下面分别介绍一下。

fd_reify注册可读写事件到epoll

fd_reify将fdchanges里面在ev_io_start里面设置记录的这些新事件一个个处理,真正加入epoll里面.所谓的reify具体化。

inline_size void fd_reify (EV_P)
{//将fdchanges里面在ev_io_start里面设置记录的这些新事件一个个处理,真正加入epoll里面.所谓的reify具体化
  int i;

  for (i = 0; i < fdchangecnt; ++i)
    {
      int fd = fdchanges [i];//只需要获取里面的fd,然后就可以在anfds里面索引描述符数据的位置了
      ANFD *anfd = anfds + fd;
      ev_io *w;

      unsigned char o_events = anfd->events;
      unsigned char o_reify  = anfd->reify;

      anfd->reify  = 0;

      /*if (expect_true (o_reify & EV_ANFD_REIFY)) probably a deoptimisation */
        {
          anfd->events = 0;

          //尝试一个个事件的去处理,看起来不能支持减少字段似得
          for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)
            anfd->events |= (unsigned char)w->events;

          if (o_events != anfd->events)
            o_reify = EV__IOFDSET; /* actually |= */
        }

        //下面调用的其实是epoll_modify, 后者根据具体情况调用epoll_ctl函数,将新事件设置到epoll里面去 ,或者减少
      if (o_reify & EV__IOFDSET)
        backend_modify (EV_A_ fd, o_events, anfd->events);
    }

  fdchangecnt = 0;
}

从上面可以看出fd_reify会遍历ev_loop->fdchanges 数组,将里面有事件改动的socket的(ev_io *)anfd->head事件列表进行整合,然后看是否跟之前有变动,如果有,则调用backend_modify修改epoll注册,实际调用的其实是epoll_modify, 后者根据具体情况调用epoll_ctl函数,将新事件设置到epoll里面去 ,或者减少。

epoll_modify在ev_epoll.c里面,其整体就是个epoll_ctl调用,再熟悉不过了。

至此,一个SOCKET的epoll事件已经加入到了epollfd中了,这样只要wait,就能监听这个事件的变化。

backend_poll监听等待SOCKET可读

backend_poll实际上是epoll_poll,这个是在整个初始化的时候在epoll_init里面设置的。epoll_poll挺简单的,做2件事情:

  • 调用epoll_wait等待监听事件通知;
  • fd_event解析事件的类型,然后放到pendings的优先级队列里面,这样到后面再慢慢处理;

注意这个epoll_poll实际上还没有触发回调函数。里面对每个可读写的fd都会调用fd_event (EV_A_ fd, got); , 最终调用fd_event_nocheck, ev_feed_event。

inline_speed void
fd_event_nocheck (EV_P_ int fd, int revents)
{//对一个SOCK进行检查,看他是不是想要revents的事件,如果想要,那么将其加入到pengdings优先级队列里面,以备后续处理
  ANFD *anfd = anfds + fd;
  ev_io *w;

  for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)
    {//看到了吧,anfd的head链表实际上就是一个个事件,比如可读,可写分别为2个事件,发生事件时会扫描这里,去寻找需要的事件,然后处>
理
      int ev = w->events & revents;

      if (ev)
        ev_feed_event (EV_A_ (W)w, ev);
    }
}
void noinline
ev_feed_event (EV_P_ void *w, int revents) EV_THROW
{//喂养事件,实际上就是将事件放到(loop)->pendings的数组里面,然后后面会一个个按照优先级去处理的
  W w_ = (W)w;
  int pri = ABSPRI (w_);

  if (expect_false (w_->pending))
    pendings [pri][w_->pending - 1].events |= revents;
  else
    {
      w_->pending = ++pendingcnt [pri];
      array_needsize (ANPENDING, pendings [pri], pendingmax [pri], w_->pending, EMPTY2);
      pendings [pri][w_->pending - 1].w      = w_;
      pendings [pri][w_->pending - 1].events = revents;
    }

  pendingpri = NUMPRI - 1;
}

从上面也可以看出libev的优先级是怎么回事,就是会在本次epoll_wait返回后,优先级高的先回调。实现方法是用ev_loop->pendings[]数组来记录不同优先级的事件。从而让后面的代码按顺序处理。

ep_run里面还用timers_reify处理了一下定时器事件,periodics_reify处理绝对时间的事件。

最后调用EV_INVOKE_PENDING 而触发各种回调函数,其实一个宏。

# define EV_INVOKE_PENDING invoke_cb (EV_A)
# define invoke_cb ((loop)->invoke_cb)

static void noinline ecb_cold
loop_init (EV_P_ unsigned int flags) EV_THROW
{//调用epoll_create创建epoll句柄//···
      invoke_cb          = ev_invoke_pending;//这个就是触发回调函数的函数,在ev_loop里面调用
      //···
}
从此可见,EV_INVOKE_PENDING实际上就是ev_invoke_pending函数,其很简单,从高优先级的开始,一个个处理pendings数组。
void noinline
ev_invoke_pending (EV_P)
{//触发在epoll_wait后加入到pendings优先级队列里面的事件,一个个一次调用他们的cb回调函数
  pendingpri = NUMPRI;

  while (pendingpri) /* pendingpri possibly gets modified in the inner loop */
    {
      --pendingpri;

      while (pendingcnt [pendingpri])
        {
          ANPENDING *p = pendings [pendingpri] + --pendingcnt [pendingpri];

          p->w->pending = 0;
          EV_CB_INVOKE (p->w, p->events);//调用回调函数
          EV_FREQUENT_CHECK;
        }
    }
}
# define EV_CB_INVOKE(watcher,revents) (watcher)->cb (EV_A_ (watcher), (revents))

这样遍历从高优先级的开始处理,从而达到优先级的效果。不过其实感觉这个没啥太多用处。因为快慢都差不多反正立即处理了。

介绍到这里差不多了,从初始化到注册可读写事件,然后进入ev_loop循环,从而对事件进行可读可写的监听而得到回调。

上面,我们分析了libev整体设计思想和主循环的工作原理,也提到了watcher是衔接开发者代码的主要入口。watcher与开发者最接近,也与具体事件处理逻辑最接近。所以,watcher的具体实现,与性能的关系也相当密切。下面,我们就来分析一下,libev中常用的几种watcher的设计与实现。

ev_io

ev_io与底层io

ev_io的主要使命就是监听并响应指定文件描述fd上的读写事件。对fd的监听工作,主要委托给底层的io库来完成。libev对目前比较流行的io库都提供了支持,如:select, epoll以及windows的iocp等。在这里libev使用了Adaptor模式,通过统一的适配层隐藏了底层io库的细节。在loop初始化的时候(loop_init),会根据配置将函数指针绑定到底层的io库函数对应的适配代码上。所以,开发者可以很方便的把代码切换到不同的底层实现上。相关的函数有:backend_modify,向底层库注册fd事件,如:epoll的epoll_ctl;backend_poll,向底层库轮询fd上是否有感兴趣的事件发生,如:epoll的epoll_wait。适配器实现的代码可以在ev_LIB.c中看到,LIB是io库的名字,如:ev_epoll.c,ev_win32.c等。

1

ev_io的结构

1234567
typedef struct ev_io{ EV_WATCHER_LIST (ev_io) int fd; /* ro */ int events; /* ro */} ev_io;

其中,EV_WATCHER_LIST是EV_WATCHER结构的链表节点结构。fd是监听的文件描述符,events是感兴趣的事件。 ev_io从它诞生的那一刻起,便于文件描述符紧密结合在一起了。ev_io的实例被存储在loop->anfds的结构中。anfds的结构如下图所示:

2

anfds其实是一个数组,它使用fd作为下标,数组中的元素是一个ANFD的结构。ANFD是一个维护fd信息的结构体。其中,events记录了当前fd上感兴趣的事件的记录。head是watcher列表的头指针,这个列表就是在这个fd上注册的watcher列表。当fd的大小超出了anfds的容量,anfds会进行相应的扩展。

anfds可以理解成一个简易的map,记录了fd与ANFD结构的映射关系。虽然,fd的申请和释放操作会导致fd不一定是连续的,从而导致数组中出现空洞,但通过fd可以迅速获取到相应的watcher列表,这也许是用空间换取时间的一个考量吧。另一方面,因为fd的释放操作并不会发出通知,而系统分配fd总是采用可用的最小fd。所以如果一个fd在别处被释放,这个fd则很有可能被分配给随后打开的其他文件。而libev对这个过程是完全不知情的,所以它会傻傻的一直认为这个fd一直指向同一个文件,默默地服务着上面发生的事件。libev不负责管理fd的改变行为,而是把这个任务交给了外面,也就是说,如果外面fd发生了改变,需要调用ev_io_set或ev_io_init来重新设定fd与watcher的关系。

ev_io的插入

从前面的介绍我们知道,要通过libev来监听fd上的事件,得要先插入一个ev_io到libev的loop中。ev_io的插入操作被封装在ev_io_start函数中。毫无疑问,libev首先会根据fd找到对应的watcher列表,并将新watcher加入到列表中。接下来,会调用fd_change函数,将fd加入到loop->fdchanges中。fdchanges是一个简单的数组,记录的是当前注册事件有发生改变的fd。到此为止,新ev_io的插入完成,上面的所有操作时间代价都是O(1)。fdchanges的作用在下面进行分析。

ev_io的选取

前面我们已经向libev的loop中插入了一个ev_io,那么libev是怎么把这个ev_io注册到底层io并响应底层的io事件的呢? 从ev_run流程图中可以看到,ev_io的选取由fd_reify和backend_poll这两个步骤来完成。

fd_reify函数的工作主要是遍历fdchanges,将对应列表的watcher的events字段合并到ANFD结构的events字段。ANFD上如果新的events与原来监听的events不一致,则表示在这个fd上监听的事件集发生了变化,需要将fd和事件集合通过backend_modify注册到底层的io库。

在循环的后面,则会调用backend_poll来检查fd上是否有注册的事件发生。如果有事件发生,则通过fd_event函数,遍历fd对应的watcher列表,比较每个watcher上的events字段与发生的事件event值,找出就绪的watcher添加到pendings中。

最后,这些pendings中的watcher会在循环结束前调用ev_invoke_pending来统一触发。

ev_io的移除

ev_io的移除由ev_io_stop来完成。首先,会先检查该watcher是否在pendings列表中,如果是,则先从pendings中删除该watcher。pendings是一个数组,libev中的数组一般是以数组和元素数量来维护的。删除数组中的一个元素,只要把数组末尾的元素替换掉被删除的元素,并把元素数量减一就可以了,操作的时间复杂度是O(1) 。

接下来就是通过fd找到watcher列表,从中删除这个watcher。这个操作需要遍历列表找到待删除的watcher,所以平均时间复杂度是O(n)。其中n是注册在fd上的watcher数量,一般这个数量不会太大。

然后是把watcher的active标志位复位,并减少全局active的watcher计数。

最后是把fd加入到fdchanges中,因为移除一个watcher,可能会改变fd上感兴趣的事件,所以要在下一轮循环中重新计算该fd上的事件集合。

ev_timer

ev_timer的管理

ev_timer watcher是主要负责处理超时事件的watcher。这类watcher被存储在loop->timers中,它们的特点是,超时时间小的watcher会被先触发。所以,timers其实是一个按触发时间升序排序的优先队列,底层的数据结构是一个用数组实现的二叉或四叉最小堆(关于堆的定义请google之)。ev_timer watcher的active字段,维护的其实是watcher在堆中的下标,通过它可以快速在堆中定位到watcher。

堆相关基本的堆操作有upheap和downheap。upheap操作是将一个节点上移到堆中合适的位置;downheap操作则刚好相反,将一个节点下移到堆中合适的位置。最小堆的特点是父节点的值比子节点的值都要小。通过这两个操作,可以调整堆中节点的位置,以满足最小堆的约束。它们的时间复杂度都是O(log(n)),堆排序时间复杂度是O(nlog(n))。有了这两个操作,便可以构建出watcher结构的常用操作了。

  • 获取超时的watcher。因为timers是一个以触发时间排序的最小堆,根部的watcher总是最先要触发的watcher。所以这个操作的主要工作就是比较根部watcher的触发时间,如果可以触发,则加入pendings队列。然后检查该watcher是否是repeat的。如果是则更新下一次触发时间,调用downheap操作将这个节点下移至合适的位置;否则直接删除该watcher。

  • 添加新的watcher。这无非就是一个入堆的操作。将新watcher添加到timers数组的末尾,再执行upheap操作,上升至合适的位置即可。

  • 删除watcher。将堆尾节点替换掉待删除节点,再根据情况用upheap或downheap操作来调整替换后节点到合适的位置。

以上这些操作的时间代价都是O(log(n))。

timer的使用策略

在实际应用中,可能会出现频繁使用大量timer的场景。比如:为每个请求设置一个超时时间,在指定时间内得不到响应,则报错。如果为每一个请求创建一个watcher,则将产生大量不必要的空间和计算开销。在libev的官方文档中,提供了一个比较高效的方法。下面简单介绍一下这种方法的思路。

可以使用一个双向链表来维护超时时间相同的timer,这里的timer可以理解为定时器的记录结构,比如:超时时间和超时的回调函数等。因为大家的超时时间是一样的,所以新的timer进来后,肯定是添加到队尾的。

然后分配一个ev_timer watcher专门来处理这个链表的超时工作。watcher的超时时间设置的是链表第一个元素的超时时间。当超时发生后,按链表顺序触发超时的timer。如果timer是重复的,可以重新计算超时时间并加入到链表尾部;否则直接删除timer记录即可。然后再从链表首部获取下一次超时时间,重复上面的流程。

采用这种方案,只需要使用一个ev_timer watcher来处理相同timeout时间的timer。而timer的增删操作最后其实就是链表的插入和删除操作,所以操作的时间代价都是O(1)。而timeout时间相同的约束,主要是要保证链表里的元素都是有序的,插入操作都是发生在链表的尾端。如果要取消某一个timer,因为是双向链表,也可以在O(1)时间内从链表内移除掉指定的节点。

ev_prepare, ev_check, ev_idle

从角色上来看,这三个类型的watcher其实都是事件循环的一个扩展点。通过这三个watcher,开发者可以在事件循环的一些特殊时刻获得回调的机会。

  • ev_prepare 在事件循环发生阻塞前会被触发。

  • ev_check 在事件循环阻塞结束后会被触发。ev_check的触发是按优先级划分的。可以保证,ev_check是同一个优先级上阻塞结束后最先被触发的watcher。所以,如果要保证ev_check是最先被执行的,可以把它的优先级设成最高。

  • ev_idle 当没有其他watcher被触发时被触发。ev_idle也是按优先级划分的。它的语义是,在当前优先级以及更高的优先级上没有watcher被触发,那么它就会被触发,无论之后在较低优先级上是否有其他watcher被触发。

这三类watcher给外部的开发者提供了非常便利的扩展机制,在这个基础上,开发者可以做很多有意思的事情,也对事件循环有了更多的控制权。具体到底能做些什么,做到什么程度,那就要看开发者们的想象力和创造力了:)

总结

ev_io和ev_timer应该是libev中使用的最多的watcher了,也是比较典型的watcher。从底层的实现上来看,处理得恰到好处,精明而干练,可谓独具匠心。

看libev的代码,最大的障碍应该是非里面漫天飞舞的宏莫属了。但把握了libev的大概结构后,知道哪些家伙长得比较像宏(比如:一些看似全局的变量),知道哪些宏要到什么地方找定义(比如:ev_vars.h,ev_wrap.h),事情就变得简单了。再回头想想,宏也是个不错的选择。第一,它是一个不错的代码解耦手段。上层代码依赖于宏,而宏在不同的环境下可以绑定到不同的底层代码,切换底层的代码而不会影响到上层代码。第二,它也是提高性能的途径。宏的绑定在预处理阶段完成,不会有额外的动态查找和函数调用开销。第三,也可以偷偷懒,用短小的宏代替一大坨代码,写的人省力。

发表评论

电子邮件地址不会被公开。

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>