Libev学习笔记2

这一节根据官方文档给出的简单示例,深入代码内部,了解其实现机制。示例代码如下:

int
main (void)
{
    struct ev_loop *loop = EV_DEFAULT;

    ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);
    ev_io_start (loop, &stdin_watcher);

    ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.);
    ev_timer_start (loop, &timeout_watcher);

    ev_run (loop, 0);

    return 0;
}

宏EV_DEFAULT对缺省的ev_loop进行初始化,然后将指针返回给main函数中的loop,它的定义为:

# define EV_DEFAULT  ev_default_loop (0)

函数ev_default_loop定义如下:

#if EV_MULTIPLICITY
struct ev_loop * ecb_cold
#else
int
#endif
ev_default_loop (unsigned int flags) EV_THROW
{
  if (!ev_default_loop_ptr)
    {
      /* EV_P = struct ev_loop *loop */
      EV_P = ev_default_loop_ptr = &default_loop_struct;

      /* 初始化ev_loop各个字段, flags = 0 */
      loop_init (EV_A_ flags);

      if (ev_backend (EV_A))
        {
#if EV_CHILD_ENABLE
          /* 接收SIGCHLD信号 */
          ev_signal_init (&childev, childcb, SIGCHLD);
          ev_set_priority (&childev, EV_MAXPRI);
          ev_signal_start (EV_A_ &childev);
          ev_unref (EV_A); /* child watcher should not keep loop alive */
#endif
        }
      else
        ev_default_loop_ptr = 0;
    }

  return ev_default_loop_ptr;
}

ev_default_loop_ptr是一个全局指针,指向缺省的ev_loop,在这个函数中可以看出,这个缺省的ev_loop就是default_loop_struct。该函数最终就是把这个ev_default_loop_ptr赋值给main函数中的loop的。初始化完指针后,就要对这个缺省ev_loop进行初始化了,这个任务交给loop_init函数来完成。loop_init函数主要是对default_loop_struct中的各个字段初始化,包括对事件驱动机制(如poll、epoll、select等)的初始化。

初始化完毕后回到main函数,此时的loop已经指向了一个初始化后的ev_loop结构体。

接下来调用ev_io_init宏函数。该函数主要针对如下几个问题进行初始化:要监听几号描述符?监听这个描述符的什么事件?监听事件发生时做什么动作?这几个问题的答案就是由我们调用该函数时所传入的参数来回答的。该宏的定义如下:

#define ev_io_init(ev,cb,fd,events)          do { ev_init ((ev), (cb)); ev_io_set ((ev),(fd),(events)); } while (0)

这个宏的初始化工作可以分为两个部分,一部分由ev_init宏来完成,另一部分由ev_io_set宏来完成。为什么要分成两部分,还需要从Libev对结构体的设计说起。

Libev中有不同的事件,每一种事件的定义不同,一个需要监控的事件称为一个watcher。在官方示例中,定义了两个watcher,一个是监控I/O的名为stdin_watcher的watcher,另一个监控定时器的名为timeout_watcher的watcher。这些watcher作为派生类,共同继承自基类EV_WATCHER,基类的定义如下:

#define EV_WATCHER(type)              int active; /* private */            \     /* 1表示watcher加入到event loop中 */
  int pending; /* private */            \    /* 1表示watcher中有事件被触发 */
  EV_DECL_PRIORITY /* private */        \    /* watcher的优先级 */
  EV_COMMON /* rw */                \        /* void *data; */
  EV_CB_DECLARE (type) /* private */         /* void (*cb)(EV_P_ struct type *w, int revents); */

实际上这个宏就是定义了一些变量,每个变量的含义我已经在注释中表明,最后一个宏展开后就是定义了一个函数指针,倒数第二个宏展开后就是一个通用指针,供用户自由使用。再看看派生类ev_io的定义:

#define EV_WATCHER_LIST(type)            \
  EV_WATCHER (type)                  struct ev_watcher_list *next; /* private */

typedef struct ev_io
{
  EV_WATCHER_LIST (ev_io)

  int fd;     /* ro */
  int events; /* ro */
} ev_io;

ev_io是一个真正的结构体,它包含的成员包括:基类中定义的各个变量,next指针,自己结构体中独有的变量。next指针的目的是将不同的watcher连接起来,形成链表。

所以,对一个派生类的初始化需要分别对继承的基类部分和派生类部分进行初始化。ev_init专门负责初始化基类部分,ev_XXX_set专门负责初始化派生类部分,XXX需要根据不同类型的watcher而定。回到刚才的ev_io_init宏,该宏分别调用了两个宏:ev_init和ev_io_set分别初始化基类成员和派生类成员。如果需要初始化timer类型的watcher,那么就调用ev_init和ev_timer_set分别进行初始化。ev_init是每一种类型的watcher都需要调用的,因为它们都继承自同一个基类,而各自特有的成员变量则通过ev_XXX_set初始化。这样的模拟面向对象的设计和各种宏的定义使得代码阅读起来变得困难,但是代码的复用性会增强,执行效率也会变得更高。

对watcher初始化完毕后回到main函数,接下来调用ev_io_start,主要代码如下:

/* EV_P_ = EV_P = struct ev_loop *loop, 单一参数用EV_P, 第一个参数用EV_P_ */
void noinline
ev_io_start (EV_P_ ev_io *w) EV_THROW
{
  int fd = w->fd;/* 设置为启动状态 */
  ev_start (EV_A_ (W)w, 1);

  /* 将watcher挂到loop->anfds的head链表中 */
  array_needsize (ANFD, anfds, anfdmax, fd + 1, array_init_zero);
  wlist_add (&anfds[fd].head, (WL)w);

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

  EV_FREQUENT_CHECK;
}

该函数主要完成两件事:

  1. 开启对stdin_watcher这个watcher的监听,包括设置相关的标志位和加入事件驱动机制中(如epoll调用epoll_ctl)。
  2. 将watcher挂到初始化好的loop中统一管理。

下面进行详细的分析。

ev_start函数就是设置一些watcher中的标志,表示开始监听该watcher。(W)w可以看成是将派生类指针ev_io*强制转换成基类指针,也就是多态机制。

在Libev中,有一个非常重要的结构体叫ANFD,它是对文件描述符的封装,一个fd对应一个ANFD,内部包含了对该fd所做动作或已触发动作的描述:

/* file descriptor info structure */
/* 一个fd对应一个ANFD */
typedef struct
{
  /* 一个fd可以有多个watcher同时监听,多个watcher形成一个链表,head表示链表头 */
  WL head;

  /* 上面链表中事件按位或 */
  unsigned char events; /* the events watched for */

  /* 为1表示需要监听的事件发生了变化,需要重新epoll_ctl */
  unsigned char reify;  /* flag set when this ANFD needs reification (EV_ANFD_REIFY, EV__IOFDSET) */

  /* 实际触发的事件 */
  unsigned char emask;  /* the epoll backend stores the actual kernel mask in here */
  unsigned char unused;
#if EV_USE_EPOLL
  unsigned int egen;    /* generation counter to counter epoll bugs */
#endif
} ANFD;

那么fd如何和对应的ANFD相关联呢?答案是将ANFD放入ev_loop的anfds数组中,fd作为下标,这样寻找对应ANFD的时间复杂度是O(1),速度非常的快。anfds数组的大小是可调的,它根据当前需要监听的fd的最大值调整自己的大小,array_needsize宏就是做调整工作的:

#define array_needsize(type,base,cur,cnt,init)              if (expect_false ((cnt) > (cur)))                    {                                      int ecb_unused ocur_ = (cur);                          (base) = (type *)array_realloc                         (sizeof (type), (base), &(cur), (cnt));              /* 只初始化新分配空间 */
      init ((base) + (ocur_), (cur) - ocur_);                }

如果新加入的fd大于anfds数组当前的大小cur,就增加容量。expect_false宏的作用是让编译期对代码在分支预测方面进行优化,提高代码性能,这涉及到流水线方面的一些知识。

回到ev_io_start函数,调整完loop的anfds数组后,调用wlist_add函数将初始化的watcher加入到anfds数组中,watcher监听的fd作为数组的下标。在ANFD的定义中有一个head成员,它就是连接所有相同fd的watcher的链表头。

回到ev_io_start函数,到目前为止,需要监听的fd还没有加入到事件驱动机制中。Libev采用的方法是将新加入的fd添加到一个数组中,之后再扫描这个数组,将数组中的所有fd加入到事件驱动机制中。ev_io_start函数中调用fd_change函数的目的就是将新fd加入到数组中,这个数组的名字叫做fdchanges,属于ev_loop的成员。不光是新的fd需要加入到fdchanges数组,被修改了监听内容的fd都需要加入到fdchanges数组。

ev_io_start函数大体上分析完毕,它的作用是开启对一个watcher的监听,而不是fd,因为一个fd可以对应多个watcher。例如监听的fd=100,需要同时监听输入和输出,那么会有两个watcher对应这个fd,一个是输入,一个是输出。ev_io_stop函数就是停止监听一个watcher,操作和ev_io_start相反:

void noinline
ev_io_stop (EV_P_ ev_io *w) EV_THROW
{
  clear_pending (EV_A_ (W)w);
  if (expect_false (!ev_is_active (w)))
    return;

  assert (("libev: ev_io_stop called with illegal fd (must stay constant after start!)", w->fd >= 0 && w->fd < anfdmax));

  EV_FREQUENT_CHECK;

  wlist_del (&anfds[w->fd].head, (WL)w);
  ev_stop (EV_A_ (W)w);

  fd_change (EV_A_ w->fd, EV_ANFD_REIFY);

  EV_FREQUENT_CHECK;
}

时间: 2024-10-14 00:46:30

Libev学习笔记2的相关文章

Libev学习笔记1

和Libevent相似,Libev是一个高性事件驱动框架,据说性能比Libevent要高,bug比Libevent要少.Libev只是一个事件驱动框架,不是网络库,因为它的内部并没有任何socket编程代码.支持的事件驱动机制包括: select poll epoll kqueue Solaris-specific event port mechanisms 支持的事件类型也很多,下面会全部列出. 官方首页地址:http://software.schmorp.de/pkg/libev.html

Libev学习笔记4

这一节分析Libev的定时器部分.对定时器的使用主要有两个函数: ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.); ev_timer_start (loop, &timeout_watcher); 和ev_io类型的watcher类似,timeout_watcher是一个类型为ev_timer的watcher,上面的ev_timer_init函数将它设置为5.5秒之后调用回调函数timeout_cb,最后一个参数0表示定时器不重复

Libev学习笔记3

设置完需要监听的事件之后,就开始event loop了.在Libev中,该工作由ev_run函数完成.它的大致流程如下: int ev_run (EV_P_ int flags) { do { /* 执行EV_FORK类型事件 */ /* 执行EV_PREPARE类型事件 */ /* 遍历fdchanges数组,使用户关注的事件和epoll关注的事件保持一致 */ fd_reify (EV_A); /* 计算epoll阻塞时间 */ /* 事件驱动机制阻塞等待事件发生 */ backend_po

libev学习笔记

libev最大的特点是采用了轮询文件描述符(select,poll,epoll,iocp,kqueue)的方式来代替线程调度和切换,省去了线程切换,效率很高. 用小顶堆而不是链表来管理定时器,有以下优势: 1.容器中的元素实现有序排列(当然链表也能做有序排列,但性能不行,参见后面几点),这样在轮询时只需要检查前几个元素,而不需要遍历所有元素. 2.小顶堆的操作,无论是插入,还是删除,时间复杂度都在O(1)~O(logN)之间.有序链表,删除的时间复杂度为O(1),但插入的时间复杂度为O(N/2)

vector 学习笔记

vector 使用练习: /**************************************** * File Name: vector.cpp * Author: sky0917 * Created Time: 2014年04月27日 11:07:33 ****************************************/ #include <iostream> #include <vector> using namespace std; int main

Caliburn.Micro学习笔记(一)----引导类和命名匹配规则

Caliburn.Micro学习笔记(一)----引导类和命名匹配规则 用了几天时间看了一下开源框架Caliburn.Micro 这是他源码的地址http://caliburnmicro.codeplex.com/ 文档也写的很详细,自己在看它的文档和代码时写了一些demo和笔记,还有它实现的原理记录一下 学习Caliburn.Micro要有MEF和MVVM的基础 先说一下他的命名规则和引导类 以后我会把Caliburn.Micro的 Actions IResult,IHandle ICondu

jQuery学习笔记(一):入门

jQuery学习笔记(一):入门 一.JQuery是什么 JQuery是什么?始终是萦绕在我心中的一个问题: 借鉴网上同学们的总结,可以从以下几个方面观察. 不使用JQuery时获取DOM文本的操作如下: 1 document.getElementById('info').value = 'Hello World!'; 使用JQuery时获取DOM文本操作如下: 1 $('#info').val('Hello World!'); 嗯,可以看出,使用JQuery的优势之一是可以使代码更加简练,使开

[原创]java WEB学习笔记93:Hibernate学习之路---Hibernate 缓存介绍,缓存级别,使用二级缓存的情况,二级缓存的架构集合缓存,二级缓存的并发策略,实现步骤,集合缓存,查询缓存,时间戳缓存

本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱好者,互联网技术发烧友 微博:伊直都在0221 QQ:951226918 -----------------------------------------------------------------------------------------------------------------

Activiti 学习笔记记录(三)

上一篇:Activiti 学习笔记记录(二) 导读:上一篇学习了bpmn 画图的常用图形标记.那如何用它们组成一个可用文件呢? 我们知道 bpmn 其实是一个xml 文件