事件库之Libev(三)

##Libev设计思路 理清了Libev的代码结构和主要的数据结构,就能够跟着示例中接口进入到Libev中,跟着代码了解其设计的思路。这里咱们管struct ev_loop称做为事件循环驱动器而将各类watcher称为事件监控器。 ###1.分析例子中的IO事件 这里在前面的例子中咱们先把定时器和信号事件的使用注释掉,只看IO事件监控器,从而了解Libev最基本的逻辑。能够结合Gdb设断点一步一步的跟看看代码的逻辑是怎样的。html

咱们从main开始一步步走。首先执行 struct ev_loop *main_loop = ev_default_loop(0); 经过跟进代码能够跟到函数 ev_default_loop 里面去,其主要逻辑,就是全局对象指针ev_default_loop_ptr若为空,也就是未曾使用预制的驱动器时,就让他指向全局对象default_loop_struct,同时在本函数里面统一用名字"loop"来表示该预制驱动器的指针。从而与函数参数为 EV_P 以及 EV_A的写法配合。接着对该指针作 loop_init操做,即初始化预制的事件驱动器。这里函数的调用了就是用到了 EV_A_ 这样的写法进行简化。初始化以后若是配置中Libev支持子进程,那么经过信号监控器实现了子进程监控器。这里能够先不用去管他,知道这段代码做用便可。 这里再Libev的函数定义的时候,会看到 "EV_THROW" 这个东西,这里能够不用管它,他是对CPP中"try ... throw"的支持,和 EV_CPP(extern "C" {)这样不一样寻常的 extern "C" 同样是一种编码技巧。如今咱们以分析设计思路为主。在了解了整体后,能够再对其编码技巧进行梳理。不然的话看一份代码会很是吃力,并且速度慢。甚至有的时候这些“hacker”并不必定是有益的。linux

1.1驱动器的初始化

下面看下驱动器的初始化过程当中都作了哪些事情。首先最开始的一段代码判断系统的clock_gettime是否支持CLOCK_REALTIME和CLOCK_MONOTONIC。这两种时间的区别在于后者不会由于系统时间被修改而被修改,详细解释能够参考man page 。接着判断环境变量对驱动器的影响,这个在官方的Manual中有提到,主要就是影响默认支持的IO复用机制。接着是一连串的初始值的赋值,开始不用了解其做用。在后面的分析过程当中即可以知道。接着是根据系统支持的IO复用机制,对其进行初始化操做。这里能够去"ev_epoll.c" 和"ev_select.c"中看一下。 最后是判断若是系统须要信号事件,那么经过一个PIPE的IO事件来实现,这里暂且不用管他,在理解了IO事件的实现后,天然就知道这里他作了什么操做。windows

对于"ev_epoll.c" 和"ev_select.c"中的 xxx_init 其本质是一致的,就像插件同样,遵循一个格式,而后能够灵活的扩展。对于epoll主要就是作了一个 epoll_create*的操做(epoll_create1能够支持EPOLL_CLOEXEC)。数组

backend_mintime = 1e-3; /* epoll does sometimes return early, this is just to avoid the worst */
backend_modify  = epoll_modify;
backend_poll    = epoll_poll;

这里就能够当作是插件的模板了,在后面会修改的时候调用backend_modify在poll的时候调用backend_poll.从而统一了操做。数据结构

epoll_eventmax = 64; /* initial number of events receivable per poll */
epoll_events = (struct epoll_event *)ev_malloc (sizeof (struct epoll_event) * epoll_eventmax)

这个就看作为是每一个机制特有的部分。熟悉epoll的话,这个就不用说了。ide

对于select (Linux平台上的)函数

backend_mintime = 1e-6;
backend_modify  = select_modify;
backend_poll    = select_poll;

这个和上面同样,是至关于插件接口oop

vec_ri  = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_ri);
vec_ro  = ev_malloc (sizeof (fd_set));
vec_wi  = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_wi);
vec_wo  = ev_malloc (sizeof (fd_set));

一样,这个是select特有的,表示读和写的fd_set的vector,ri用来装select返回后符合条件的部分。其余的如poll、kqueue、Solaris port都是相似的,能够自行阅读。this

####1.2IO监控器的初始化编码

上面的过程执行完了ev_default_loop过程,而后到后面的ev_init(&io_w,io_action);,他不是一个函数,而是一个宏定义:

((ev_watcher *)(void *)(ev))->active = ((ev_watcher *)(void *)(ev))->pending = 0;
ev_set_priority ((ev), 0);
ev_set_cb ((ev), cb_);

这里虽然还有两个函数的调用,可是很好理解,就是设置了以前介绍的基类中 "active"表示是否激活该watcher,“pending”该监控器是否处于pending状态,"priority"其优先级以及触发后执行的动做的回调函数。

####1.3 设置IO事件监控器的触发条件 在初始化监控器后,还要设置其监控监控的条件。当该条件知足时便触发该监控器上注册的触发动做。ev_io_set(&io_w,STDIN_FILENO,EV_READ);从参数边能够猜出他干了什么事情。就是设置该监控器监控标准输入上的读事件。该调用也是一个宏定义:

(ev)->fd = (fd_); (ev)->events = (events_) | EV__IOFDSET;

就是设置派生类IO监控器特有的变量fd和events,表示监控那个文件fd已经其上的可读仍是可写事件。 %TODO:补上EV_IOFDSET的做用

1.4注册IO监控器到事件驱动器上

准备好了监控器后就要将其注册到事件驱动器上,这样就造成了一个完整的事件驱动模型。 ev_io_start(main_loop,&io_w); 。这个函数里面会第一次见到一个一个宏 "EV_FREQUENT_CHECK",是对函数 "ev_verify"的调用,那么ev_verify是干什么的呢?用文档的话“This can be used to catch bugs inside libev itself”,若是看其代码的话,就是去检测Libev的内部数据结构,判断各边界值是否合理,不合理的时候assert掉。在生产环境下,我以为根据性格来对待。若是以为他消耗资源(要检测不少东西跑不少循环)能够编译的时候关掉该定义。若是须要assert,能够在编译的时候加上选项。

而后看到 ev_start 调用,该函数实际上就是给驱动器的loop->activecnt增一并置loop->active为真(这里统一用loop表示全局对象的预制驱动器对象default_loop_struct),他们分别表示事件驱动器上正监控的监控器数目以及是否在为监控器服务。

array_needsize (ANFD, anfds, anfdmax, fd + 1, array_init_zero);
wlist_add (&anfds[fd].head, (WL)w);

感兴趣的能够去看下Libev里么动态调整数组的实现。这里咱们主要看总体逻辑。他的工做过程是先判断数组anfds是否还有空间再加对文件描述符fd的监控,,没有的话则调整数组的内存大小,使其大小足以容下。

这里要介绍下以前没有介绍的一个数据结构,这个没有上下文比较难理解,所以放在这里介绍。

typedef struct
{
  WL head;
  unsigned char events; /* the events watched for */
  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;
  unsigned int egen;    /* generation counter to counter epoll bugs */
} ANFD;  /* 这里去掉了对epoll的判断和windows的IOCP*/

这里首先只用关注一个 "head" ,他是以前说过的wather的基类链表。这里一个ANFD就表示对一个文件描述符的监控,那么对该文件描述的可读仍是可写监控,监控的动做是如何定义的,就是经过这个链表,把对该文件描述法的监控器都挂上去,这样就能够经过文件描述符找到了。而前面的说的anfds就是这个对象的数组,下标经过文件描述符fd进行索引。在Redis-ae那篇文章中已经讨论过这样的能够达到O(1)的索引速度并且空间占用也是合理的。

接着的“fd_change”与“fd_reify”是呼应的。前者将fd添加到一个fdchanges的数组中,后者则依次遍历这个数组中的fd上的watcher与anfds里面对饮的watcher进行对比,判断监控条件是否改变了,若是改变了则调用backend_modify也就是epoll_ctl等调整系统对该fd的监控。这个fdchanges数组的做用就在于此,他记录了anfds数组中的watcher监控条件可能被修改的文件描述符,并在适当的时候将调用系统的epoll_ctl或则其余文件复用机制修改系统监控的条件。这里咱们把这两个主要的物理结构梳理下: anfds的结构

总结一下注册过程就是经过以前设置了监控条件IO watcher得到监控的文件描述符fd,找到其在anfds中对应的ANFD结构,将该watcher挂到该结构的head链上。因为对应该fd的监控条件有改动了,所以在fdchanges数组中记录下该fd,在后续的步骤中调用系统的接口修改对该fd监控的条件。

####1.5 启动事件驱动器

一切准备就绪了就能够开始启动事情驱动器了。就是 ev_run。 其逻辑很清晰。就是

do{
    xxxx;
    backend_poll();   
    xxxx
}while(condition_is_ok)

循环中开始一段和fork 、 prepare相关这先直接跳过,到分析与之相关的监控事件才去看他。直接到 /* calculate blocking time */ 这里。熟悉事件模型的话,这里仍是比较常规的。就是从定时器堆中取得最近的时间(固然这里分析的时候没有定时器)与loop->timeout_blocktime比较获得阻塞时间。这里若是设置了驱动器的io_blocktime,那么在进入到poll以前会先sleep io_blocktime时间从而等待IO或者其余要监控的事件准备。这里进入到backend_poll中的阻塞时间是包括了io_blocktime的时间。而后进入到backend_poll中。对于epoll就是进入到epoll_wait里面。

epoll(或者select、kqueue等)返回后,将监控中的文件描述符fd以及其pending(知足监控)的条件经过 fd_event作一个监控条件是否改变的判断后到fd_event_nocheck里面对anfds[fd]数组中的fd上的挂的监控器依次作检测,若是pending条件符合,便经过ev_feed_event将该监控器加入到pendings数组中pendings[pri]上的pendings[pri][old_lenght+1]的位置上。这里要介绍一个新的数据结构,他表示pending中的wather也就是监控条件知足了,可是尚未触发动做的状态。

typedef struct
{
  W w;
  int events; /* the pending event set for the given watcher */
} ANPENDING;

这里 W w应该知道是以前说的基类指针。pendings就是这个类型的一个二维数组数组。其以watcher的优先级为一级下标。再以该优先级上pengding的监控器数目为二级下标,对应的监控器中的pending值就是该下标加一的结果。其定义为 ANPENDING *pendings [NUMPRI]。同anfds同样,二维数组的第二维 ANPENDING *是一个动态调整大小的数组。这样操做以后。这个一系列的操做能够认为是fd_feed的后续操做,xxx_reify目的最后都是将pending的watcher加入到这个pengdings二维数组中。后续的几个xxx_reify也是同样,等分析到那个类型的监控器类型时在做展开。 这里用个图梳理下结构。 pendings结构梳理图

最后在循环中执行宏EV_INVOKE_PENDING,实际上是调用loop->invoke_cb,若是没有自定义修改的话(通常不会修改)就是调用ev_invoke_pending。该函数会依次遍历二维数组pendings,执行pending的每个watcher上的触发动做回调函数。

至此一次IO触发过程就完成了。

###2总结出Libev的设计思路

在Libev中watcher要算最关键的数据结构了,整个逻辑都是围绕着watcher作操做。Libev内部维护一个基类ev_wathcer和若干个特定监控器的派生类ev_xxx。在使用的时候首先生成一个特定watcher的实例。并经过该派生对象私有的成员设置其触发条件。而后用anfds或者最小堆管理这些watchers。而后Libev经过backend_poll以及时间堆管理运算出pending的watcher。而后将他们加入到一个以优先级为一维下标的二维数组。在合适的时间依次调用这些pengding的watcher上注册的触发动做回调函数,这样即可以按优先级前后顺序实现“only-for-ordering”的优先级模型。 思路流程图

相关文章
相关标签/搜索