libev:详解
事件库之Libev(一)
使用Libev
Libev的作者写了一份很好的官方Manual,比较的齐全,即介绍了Libev的设计思想,也介绍了基本使用还包括内部各类事件详细介绍。这里略微赘述一下。Libev通过一个 ·struct ev_loop· 结结构表示一个事件驱动的框架。在这个框架里面通过ev_xxx结构,ev_init、ev_xxx_set、ev_xxx_start接口箱这个事件驱动的框架里面注册事件监控器,当相应的事件监控器的事件出现时,便会触发该事件监控器的处理逻辑,去处理该事件。处理完之后,这些监控器进入到下一轮的监控中。符合一个标准的事件驱动状态的模型。
Libev 除了提供了基本的三大类事件(IO事件、定时器事件、信号事件)外还提供了周期事件、子进程事件、文件状态改变事件等多个事件,这里我们用三大基本事件写一个例子,和Manual上的类似,但是没有做收尾工作,为的是将事件的框架清晰的呈现出来。
点击(此处)折叠或打开
- #include<ev.h>
- #include <stdio.h>
- #include <signal.h>
- #include <sys/unistd.h>
- ev_io io_w;
- ev_timer timer_w;
- ev_signal signal_w;
- void io_action(struct ev_loop *main_loop,ev_io *io_w,int e)
- {
- int rst;
- char buf[1024] = {‘\0‘};
- puts("in io cb\n");
- read(STDIN_FILENO,buf,sizeof(buf));
- buf[1023] = ‘\0‘;
- printf("Read in a string %s \n",buf);
- ev_io_stop(main_loop,io_w);
- }
- void timer_action(struct ev_loop *main_loop,ev_timer *timer_w,int e)
- {
- puts("in tiemr cb \n");
- ev_timer_stop(main_loop,timer_w);
- }
- void signal_action(struct ev_loop *main_loop,ev_signal signal_w,int e)
- {
- puts("in signal cb \n");
- ev_signal_stop(main_loop,signal_w);
- ev_break(main_loop,EVBREAK_ALL);
- }
- int main(int argc ,char *argv[])
- {
- struct ev_loop *main_loop = ev_default_loop(0);
- ev_init(&io_w,io_action);
- ev_io_set(&io_w,STDIN_FILENO,EV_READ);
- ev_init(&timer_w,timer_action);
- ev_timer_set(&timer_w,2,0);
- ev_init(&signal_w,signal_action);
- ev_signal_set(&signal_w,SIGINT);
- ev_io_start(main_loop,&io_w);
- ev_timer_start(main_loop,&timer_w);
- ev_signal_start(main_loop,&signal_w);
- ev_run(main_loop,0);
- return 0;
- }
下面对使用到的这些API进行说明。
这里使用了3种事件监控器,分别监控IO事件、定时器事件以及信号事件。因此定义了3个监控器(watcher),以及触发监控器时要执行动作的回调函数。Libev定义了多种监控器,命名方式为 ev_xxx 这里xxx代表监控器类型,其实现是一个结构体,
点击(此处)折叠或打开
- typedef struct ev_io
- {
- ....
- } ev_io
通过宏定义可以简写为 ev_xxx。回调函数的类型为 void cb_name(struct ev_loop *main_loop,ev_xxx *io_w,int event) 。
在main中,首先定义了一个事件驱动器的结构 struct ev_loop *main_loop 这里调用 ev_default_loop(0)生成一个预制的全局驱动器。这里可以参考Manual中的选择。然后依次初始化各个监控器以及设置监控器的触发条件。
初始化监控器的过程是将相应的回调函数即触发时的动作注册到监控器上。
设置触发条件则是该条件产生时才去执行注册到监控器上的动作。对于IO事件,一般是设置特定fd上的的可读或可写事件,定时器则是多久后触发。这里定时器的触发条件中还有第三参数,表示第一次触发后,是否循环,若为0则吧循环,否则按该值循环。信号触发器则是设置触发的信号。
在初始化并设置好触发条件后,先调用ev_xxx_start 将监控器注册到事件驱动器上。接着调用 ev_run 开始事件驱动器。
在事件的触发动作里面。我加入了一个 ev_xxx_stop 函数,与上面对应,也就是讲改监控器从事件驱动器里面注销掉。使其不再起作用。而在信号触发的动作中还加入了一个 ev_break 该函数可以使进程跳出 main_loop 事件驱动器循环,也就是关闭事件驱动器。结束这一逻辑。
libev最简单的示例就是这样的一个结构。定义一个监控器、书写触发动作逻辑、初始化监控器、设置监控器触发条件、将监控器加入大事件驱动器的循环中即可。一个比较清晰的事件驱动框架。
libev的事件驱动过程可以想象成如下的伪代码:
点击(此处)折叠或打开
- do_some_init()
- is_run = True
- while is_run:
- t = caculate_loop_time()
- deal_loop(t)
- deal_with_pending_event()
- do_some_clear()
首先做一些初始化操作,然后进入到循环中,该循环通过一个状态位来控制是否执行。在循环中,计算出下一次轮询的时间,这里轮询的实现就采用了系统提供的epoll、kqueue等机制。再轮询结束后检查有哪些监控器的被触发了,依次执行触发动作。这里不要纠结信号事件、定时器时间咋都经过了 deal_loop libev是如何实现的这里暂且不讨论,这个伪代码只是大致表示下libev的整体框架。
事件库之Libev(二)
目录[-]
Libev源代码结构
对于毕业生,尤其是没有接触过一些已有工程代码的新人。拿到一份源码,怎么去熟悉它是首要解决的问题。我一般把会把源码进行分类:一类是产品类的,就比如Redis、Ngnix这一类本身是一个完整的可以运维的成熟产品;另一类就是Libev这样的组件类的。对于组件类的项目,我一般就是分成这样几步:
- 有文档看文档,没有文档问相关人员(包括Google),这个组件主要提供什么服务
- 结合上述信息使用组件的AIP写个示例程序,跑起来
- 大致浏览下源码,分析一下代码的组织结构
- 根据使用的API,进到源码中看看主干是怎么样实现的,从而了解整体思路
- 再搜刮源码,把一些辅助的功能看下,并在例子中尝试
- 之后将整个理解用文字记录下来。提炼两大块内容:实现思想和技巧tips
这里我对Libev的学习就是依照这样的一个逻辑一步一步走的。
ev.c代码结构
在“使用Libev” 这篇文章中提到了一个Libev的官方文档,并根据该文档写了个简单的示例,包括了IO事件、定时器事件以及信号事件这3个最常用的事件类型。在本篇文章中将对Libev的代码结构进行分析。
首先下载Libev的源码包,下载回来后进行解压,Libev的源码都放在同一个目录中,除去autoconfig产生的文件,代码文件还是比较直观的。主要的.c和.h文件从命名上也查不多能猜出来干嘛呢。根据我们的例子,主要抽出其中的"ev.c ev_epoll.c ev_select.c ev.h ev_wrap.c ev_vars.c"结合我们的例子进行梳理。
“ev_epoll.c"和"ev_select.c"是对系统提供的IO复用机制“epoll”、“select"的支持,还有"poll”、“kqueue” Solaris的"port"的支持,分别是"ev_poll.c”、“ev_kqueue.c”、“ev_port.c”。具体的框架是类似的,因此只要分析一个其他的就都了解了。
“ev.h” 是对一些API和变量、常量的定义,“ev.c"是Libev的主要逻辑,其中在类型的定义的时候用了一个宏的包装来声明成员变量,在文件"ev_vars.c”
中。为了对成员变量使用的语句进行简化,就又写了一个"ev_wrap.c”。因此我们可以这样去看待这些文件,主要逻辑都在"ev.c”,其中部分常量、变量的定义可以在"ev.h"中,有个结构的成员变量部分的定义在"ev_vars.c"中,同时对该结构成员变量的引用通过"ev_wrap.c"文件做了个简写的宏定义;当需要系统提供底层的事件接口时,按分类分别在"ev_epoll.c”、“ev_select.c"等文件中。
接着打开"ev.c"文件,“ev.h"里面的各种定义,在需要的时候去查询即可,通过IDE或者Vim/Emacs结合cscope/ctag都可以很好的解决。通过浏览可以发现这些代码大概可以分成三部分:
因此可以直接跳到代码部分。分隔点有ecb结束的注释。这可以不用担心略过的部分,等需要的时候回过去查阅即可。其中ecb的部分,只要知道其API作用即可,无需深究,如果未来需要的时候可以到这边来做一个参考。
这样对整体的布局有个大概的了解,就可以有选择性的逐个突破了。这里还可以结合官方的文档去了解下每个函数作用。从而对Libev的整体提供的服务有个大概的了解。
主要数据结构
浏览的过程中梳理下几个重要的数据结构
1.时间类型?
点击(此处)折叠或打开
- typedef double ev_tstamp
2.坑爹的 EV_XX_
Libev用ev_tstamp表示时间单位,其实质就是一个double类型变量。
点击(此处)折叠或打开
- 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 */
这里的定义还是比较让人无解的。“EV_XXX” 等同于 EV_XXX,,这样在后续的API使用中,会显的更简洁一些,比如针对第一个参数是struct ev_loop *loop 的回调函数的书写,就可以写成 · void io_action(EV_P ev_io *io_w,int e)· 。这里不知道作者还有没有其他用以,这里我不是很推荐,但是要知道,后面再看代码的时候才更容易理解。
3.各种watcher
基类首先看一个ev_watcher,这个我们可以用OO思想去理解他,他就相当于一个基类,后续的ev_io什么的都是派生自该机构,这里利用了编译器的一个“潜规则”就是变量的定义顺序与声明顺序一致。这一点在libuv里面也用了,然后大神云风哥还对其吐槽了一番,可以参见云风的blog。这里我尽量吧所有宏包裹的部分都拨出来,方便理解和看。看过Libev的代码,我想在惊叹其宏的高明之余一定也吐槽过。
点击(此处)折叠或打开
- typedef struct ev_watcher
- {
- int active;
- int pending;
- int priority;
- void *data;
- void (*cb)(struct ev_loop *loop, struct ev_watcher *w, int revents);
- } ev_watcher
与基类配套的还有个装监控器的List。
点击(此处)折叠或打开
- typedef struct ev_watcher_list
- {
- int active;
- int pending;
- int priority;
- void *data;
- void (*cb)(struct ev_loop *loop, struct ev_watcher_list *w, int revents);
- struct ev_watcher_list *next;
- } ev_watcher_list
点击(此处)折叠或打开
- typedef struct ev_io
- {
- int active;
- int pending;
- int priority;
- void *data;
- void (*cb)(struct ev_loop *loop, struct ev_io *w, int revents);
- struct ev_watcher_list *next;
- int fd; /* 这里的fd,events就是派生类的私有成员,分别表示监听的文件fd和触发的事件(可读还是可写) */
- int events;
- } ev_io
在这里,通过从宏中剥离出来后,可以看到将派生类的私有变量放在了共有部分的后面。这样,当使用C的指针强制转换后,一个指向 struct ev_io对象的基类 ev_watcher 的指针p就可以通过 p->active 访问到派生类中同样表示active的成员了。
定时器watcher点击(此处)折叠或打开
- typedef struct ev_watcher_time
- {
- int active;
- int pending;
- int priority;
- void *data;
- void (*cb)(struct ev_loop *loop, struct ev_watcher_time *w, int revents);
- ev_tstamp at; /* 这个at就是派生类中新的自有成员 ,表示的是at时间触发 */
- } ev_watcher_time
这里定时器事件watcher和IO的不一样的地方在于,对于定时器会用专门的最小堆去管理。而IO和信号等其他事件的监控器则是通过单链表挂起来的,因此他没有next成员。
信号watcher点击(此处)折叠或打开
- typedef struct ev_signal
- {
- int active;
- int pending;
- int priority;
- void *data;
- void (*cb)(struct ev_loop *loop, struct ev_signal *w, int revents);
- struct ev_watcher_list *next;
- int signum; /* 这个signum就是派生类中新的自有成员 ,表示的是接收到的信号,和定时器中的at类似 */
- } ev_signal
还有其他的事件watcher的数据结构也是和这个类似的,可以对着"ev.h"的代码找一下,这里不再赘述了。最后看一个可以容纳所有监控器对象的类型:
点击(此处)折叠或打开
- union ev_any_watcher
- {
- struct ev_watcher w;
- struct ev_watcher_list wl;
- struct ev_io io;
- struct ev_timer timer;
- struct ev_periodic periodic;
- struct ev_signal signal;
- struct ev_child child;
- struct ev_stat stat;
- struct ev_idle idle;
- struct ev_prepare prepare;
- struct ev_check check;
- struct ev_fork fork;
- struct ev_cleanup cleanup;
- struct ev_embed embed;
- struct ev_async async;
- }
4.最重要的 ev_loop
在上面就已经看到了 struct ev_loop 的前向声明了,那么他到底是怎样的一个结构的?在“ev.c”里面可以看到这样的定义:
点击(此处)折叠或打开
- 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"和"ev_wrap.h"是为了定义一个数据结构及简化访问其成员的,就是说的这个 ev_loop 结构体。
这里用的宏为:
点击(此处)折叠或打开
- #define VAR(name,decl) decl;
- #define VARx(type,name) VAR(name, type name)
展开就是
点击(此处)折叠或打开
- #define VARx(type,name) type name
然后再看"ev_vars.h” ,里面都是 类型-变量的 VARx的宏,这样再将其include 到结构体的定义中。这样就可以看成该结构定义为:
点击(此处)折叠或打开
- struct ev_loop
- {
- ev_tstamp ev_rt_now;
- ev_tstamp now_floor;
- int rfeedmax;
- ... .........;
- }
不知道作者的用意何在,目前还没有看到这样做的好处在哪里。
然后 #define ev_rt_now ((loop)->ev_rt_now) 可以和后面的 “ev_warp.h"一起看。实际上就是 #define xxx ((loop)->xxx) 这样在要用struct ev_loop 的一个实例对象loop的成员时,就可以直接写成xxx了,这里再联想到之前的 EV_P EV_P_ EV_A EV_A_ ,就会发现,在Libev的内部函数中,这样的配套就可以使代码简洁不少。不过这样也增加了第一次阅读其的门槛。相信没有看过Libev不说其晦涩的。
5.重要的全局变量
default_loop_struct在"ev.c"中有
点击(此处)折叠或打开
- static struct ev_loop default_loop_struct
这个就是strct loop的一个实例对象,表示的是预制事件驱动器。如果在代码中使用的是预制事件驱动器,那么后续的操作就都围绕着这个数据结构展开了。
为了操作方便,还定义了指向该对象的一个全局指针:
点击(此处)折叠或打开
- struct ev_loop *ev_default_loop_ptr
代码的框架和主要的数据结构梳理出来了,还有ANFD、ANHEAP等数据结构在后面分析具体监控器是的时候在详细介绍。后面就要跟进程序的逻辑从而了解其设计思想,这样便可以深入的了解一款组件型的开源软件了。
事件库之Libev(三)
Libev设计思路
理清了Libev的代码结构和主要的数据结构,就可以跟着示例中接口进入到Libev中,跟着代码了解其设计的思路。这里我们管struct ev_loop称作为事件循环驱动器而将各种watcher称为事件监控器。
1.分析例子中的IO事件
这里在前面的例子中我们先把定时器和信号事件的使用注释掉,只看IO事件监控器,从而了解Libev最基本的逻辑。可以结合Gdb设断点一步一步的跟看看代码的逻辑是怎样的。
我们从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”并不一定是有益的。
1.1驱动器的初始化
下面看下驱动器的初始化过程中都做了哪些事情。首先最开始的一段代码判断系统的clock_gettime是否支持CLOCK_REALTIME和CLOCK_MONOTONIC。这两种时间的区别在于后者不会因为系统时间被修改而被修改,详细解释可以参考man page 。接着判断环境变量对驱动器的影响,这个在官方的Manual中有提到,主要就是影响默认支持的IO复用机制。接着是一连串的初始值的赋值,开始不用了解其作用。在后面的分析过程中便可以知道。接着是根据系统支持的IO复用机制,对其进行初始化操作。这里可以去"ev_epoll.c” 和"ev_select.c"中看一下。 最后是判断如果系统需要信号事件,那么通过一个PIPE的IO事件来实现,这里暂且不用管他,在理解了IO事件的实现后,自然就知道这里他做了什么操作。
对于"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的话,这个就不用说了。
对于select (Linux平台上的)
点击(此处)折叠或打开
- backend_mintime = 1e-6;
- backend_modify = select_modify;
- backend_poll = select_poll
这个和上面一样,是相当于插件接口
点击(此处)折叠或打开
- 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都是类似的,可以自行阅读。
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*/
接着的“fd_change”与“fd_reify”是呼应的。前者将fd添加到一个fdchanges的数组中,后者则依次遍历这个数组中的fd上的watcher与anfds里面对饮的watcher进行对比,判断监控条件是否改变了,如果改变了则调用backend_modify也就是epoll_ctl等调整系统对该fd的监控。这个fdchanges数组的作用就在于此,他记录了anfds数组中的watcher监控条件可能被修改的文件描述符,并在适当的时候将调用系统的epoll_ctl或则其他文件复用机制修改系统监控的条件。这里我们把这两个主要的物理结构梳理下:
总结一下注册过程就是通过之前设置了监控条件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也是一样,等分析到那个类型的监控器类型时在作展开。
这里用个图梳理下结构。
最后在循环中执行宏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”的优先级模型。
事件库之Libev(四)
另外两个重要的监控器
前面通过IO监控器将Libev的整个工作流程过了一遍。中间滤过了很多与其他事件监控器相关的部分,但是整体思路以及很明晰了,只要针对其他类型的watcher看下其初始化和注册过程以及在ev_run中的安排即可。这里我们再分析另两个常用的watcher
1.分析定时器监控器
定时器在程序中可以做固定周期tick操作,也可以做一次性的定时操作。Libev中与定时器类似的还有个周期事件watcher。其本质都是一样的,只是在时间的计算方法上略有不同,并有他自己的一个事件管理的堆。对于定时器事件,我们按照之前说的顺序从ev_init开始看起。
1.1定时器监控器的初始化
定时器初始化使用 ev_init(&timer_w,timer_action);,这个过程和之前的IO类似,主要就是设置基类的active、pending、priority以及触发动作回调函数cb。
1.2设置定时器监控器的触发条件
通过 ev_timer_set(&timer_w,2,0);可以设置定时器在2秒钟后被触发。如果第三个参数不是0而是一个大于0的正整数n时,那么在第一次触发(2秒后),每隔n秒会再次触发定时器事件。
其为一个宏定义 do { ((ev_watcher_time *)(ev))->at = (after_); (ev)->repeat = (repeat_); } while (0) 也就是设置派生类定时器watcher的“at”为触发事件,以及重复条件“repeat”。
1.3将定时器注册到事件驱动器上
ev_timer_start(main_loop,&timer_w);会将定时器监控器注册到事件驱动器上。其首先 ev_at (w) += mn_now; 得到未来的时间,这样放到时间管理的堆“timers”中作为权重。然后通过之前说过的“ev_start”修改驱动器loop的状态。这里我们又看到了动态大小的数组了。Libev的堆的内存管理也是通过这样的关系的。具体这里堆的实现,感兴趣的可以仔细看下实现。这里的操作就是将这个时间权重放到堆中合适的位置。这里堆单元的结构为:
点击(此处)折叠或打开
- typedef struct {
- ev_tstamp at;
- WT w;
- } ANHE
其实质就是一个时刻at上挂一个放定时器watcher的list。当超时时会依次执行这些定时器watcher上的触发回调函数。
1.4定时器监控器的触发
最后看下在一个事件驱动器循环中是如何处理定时器监控器的。这里我们依然抛开其他的部分,只找定时器相关的看。在“/ calculate blocking time /”块里面,我们看到计算blocking time的时候会先:
点击(此处)折叠或打开
- if (timercnt) {
- ev_tstamp to = ANHE_at (timers [HEAP0]) - mn_now;
- if (waittime > to) waittime = to;
- }
如果有定时器,那么就从定时器堆(一个最小堆)timers中取得堆顶上最小的一个时间。这样就保证了在这个时间前可以从backend_poll中出来。出来后执行timers_reify处理将pengding的定时器。
在timers_reify中依次取最小堆的堆顶,如果其上的ANHE.at小于当前时间,表示该定时器watcher超时了,那么将其压入一个数组中,由于在实际执行pendings二维数组上对应优先级上的watcher是从尾往头方向的,因此这里先用一个数组依时间先后次存下到一个中间数组loop->rfeeds中。然后将其逆序调用ev_invoke_pending插入到pendings二维数组中。这样在执行pending事件的触发动作的时候就可以保证,时间靠前的定时器优先执行。函数 feed_reverse和 feed_reverse_done就是将超时的定时器加入到loop->rfeeds暂存数组以及将暂存数组中的pending的watcher插入到pengdings数组的操作。把pending的watcher加入到pendings数组,后续的操作就和之前的一样了。回依次执行相应的回调函数。
这个过程中还判断定时器的 w->repeat 的值,如果不为0,那么会重置该定时器的时间,并将其压入堆中正确的位置,这样在指定的时间过后又会被执行。如果其为0,那么调用ev_timer_stop关闭该定时器。 其首先通过clear_pending置pendings数组中记录的该watcher上的回调函数为一个不执行任何动作的哑动作。
总结一下定时器就是在backend_poll之前通过定时器堆顶的超时时间,保证blocking的时间不超过最近的定时器时间,在backend_poll返回后,从定时器堆中取得超时的watcher放入到pendings二维数组中,从而在后续处理中可以执行其上注册的触发动作。然后从定时器管理堆上删除该定时器。最后调用和ev_start呼应的ev_stop修改驱动器loop的状态,即loop->activecnt减少一。并将该watcher的active置零。
对于周期性的事件监控器是同样的处理过程。只是将timers_reify换成了periodics_reify。其内部会对周期性事件监控器派生类的做类似定时器里面是否repeat的判断操作。判断是否重新调整时间,或者是否重复等逻辑,这些看下代码比较容易理解,这里不再赘述。·
2.分析信号监控器
分析完了定时器的部分,再看下另一个比较常用的信号事件的处理。Libev里面的信号事件和Tornado.IOLoop是一样的,通过一个pipe的IO事件来处理。直白的说就是注册一个双向的pipe文件对象,然后监控上面的读事件,待相应的信号到来时,就往这个pipe中写入一个值然他的读端的读事件触发,这样就可以执行相应注册的触发动作回调函数了。
我们还是从初始化-》设置触发条件-》注册到驱动器-》触发过程这样的顺序介绍。
2.1信号监控器的初始化
ev_init(&signal_w,signal_action);这个函数和上面的一样不用说了
2.2设置信号监控器的触发条件
ev_signal_set(&signal_w,SIGINT);该函数设置了Libev收到SIGINT信号是触发注册的触发动作回调函数。其操作和上面的一样,就是设置了信号监控器私有的(ev)->signum为标记。
2.3将信号监控器注册到驱动器上
这里首先介绍一个数据结构:
点击(此处)折叠或打开
- typedef struct
- {
- EV_ATOMIC_T pending;
- EV_P;
- WL head;
- } ANSIG;
- static ANSIG signals [EV_NSIG - 1]
EV_ATOMIC_T pending;可以认为是一个原子对象,对他的读写是原子的。一个表示事件驱动器的loop,以及一个watcher的链表。
在ev_signal_start中,通过signals数组存储信号监控单元。该数组和anfds数组类似,只是他以信号值为索引。这样可以立马找到信号所在的位置。从 Linux 2.6.27以后,Kernel提供了signalfd来为信号产生一个文件描述符从而可以用文件复用机制epoll、select等来管理信号。Libev就是用这样的方式来管理信号的。 这里的代码用宏控制了。其逻辑大体是这样的
点击(此处)折叠或打开
- #if EV_USE_SIGNALFD
- res = invoke_signalfd
- # if EV_USE_SIGNALFD
- if (res is not valied)
- # endif
- {
- use evpipe to instead
- }
这个是框架。其具体的实现可以参考使用signalfd和evpipe_init实现。其实质就是通过一个类似于管道的文件描述符fd,设置对该fd的读事件监听,当收到信号时通过signal注册的回调函数往该fd里面写入,使其读事件触发,这样通过backend_poll返回后就可以处理ev_init为该信号上注册的触发回调函数了。
在函数evpipe_init里面也用了一个可以学习的技巧,和上面的#if XXX if() #endif {} 一样,处理了不支持eventfd的情况。eventfd是Kernel 2.6.22以后才支持的系统调用,用来创建一个事件对象实现,进程(线程)间的等待/通知机制。他维护了一个可以读写的文件描述符,但是只能写入8byte的内容。但是对于我们的使用以及够了,因为这里主要是获得其可读的状态。对于不支持eventfd的情况,则使用上面说过的,用系统的pipe调用产生的两个文件描述符分别做读写对象,来完成。
2.4信号事件监控器的触发
在上面设置信号的pipe的IO事件是,根据使用的机制不同,其实现和触发有点不同。对于signalfd。
点击(此处)折叠或打开
- ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ); /* for signalfd */
- ev_set_priority (&sigfd_w, EV_MAXPRI);
- ev_io_start (EV_A_ &sigfd_w)
也就是注册了sigfdcb函数。该函数:
点击(此处)折叠或打开
- ssize_t res = read (sigfd, si, sizeof (si));
- for (sip = si; (char *)sip < (char *)si + res; ++sip)
- ev_feed_signal_event (EV_A_ sip->ssi_signo)
首先将pipe内容读光,让后续的可以pengding在该fd上。然后对该signalfd上的所有信号弟阿勇ev_feed_signal_event吧每个信号上的ANSIG->head上挂的watcher都用ev_feed_event加入到pendings二维数组中。这个过程和IO的完全一样。
而对于eventfd和pipe则是:
点击(此处)折叠或打开
- ev_init (&pipe_w, pipecb);
- ev_set_priority (&pipe_w, EV_MAXPRI);
- ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ);
- ev_io_start (EV_A_ &pipe_w)
pipe_w是驱动器自身的loop->pipe_w。并为其设置了回调函数pipecb:
点击(此处)折叠或打开
- #if EV_USE_EVENTFD
- if (evpipe [0] < 0)
- {
- uint64_t counter;
- read (evpipe [1], &counter, sizeof (uint64_t));
- }
- else
- #endif
- {
- char dummy[4];
- read (evpipe [0], &dummy, sizeof (dummy));
- }
- ...
- xxx
- ...
- for (i = EV_NSIG - 1; i--; )
- if (expect_false (signals [i].pending))
- ev_feed_signal_event (EV_A_ i + 1)
这里将上面的技巧#if XXX if() #endif {}拓展为了#if XXX if() {} else #endif {} 。这里和上面的操作其实是一样的。后续操作和signalfd里面一样,就是读光pipe里面的内容,然后依次将watcher加入到pendings数组中。
事件库之Libev(五)
其他监控器
最主要的几个监控器搞定了。其他的我觉得比较可以看的还有ev_child和ev_stat。其实和之前的三个基本原理的是一样。暂不赘述。未来可能补充。
Libev中的Tips
如果将Libev当成组件去用的话。官方文档是一份很好的选择。这里说下看Libev过程中的感受。
如果使用Libev但又觉得它没有提供必要的功能而要去该其代码。可能Libuv为我们做了一个很好的示例。Libuv之前是用Libev作为其底层事件库。后来作者重写了自己的一套网络库Libuv。严格意义上说,Libev仅仅是一个事件模型框架,并不能算上是一个完整的网络库,正因为如此他才提供了如此多的事件类型。而对于网络库可能最重要的就是定时器、IO、以及信号事件。当然网络还包括了socket、收发控制等内容。因此,我的感觉是可以将Libev当成一个很好的学习对象,不论是其设计思想、还是代码中个各种小tips、还有其对跨平台支持的方法都是很好的示例。虽然用宏包裹的比较严密,只要稍加分析,理清其思路还是比较容易的。
将Libev和之前的Redis-ae进行对比。可以发现Libev在设计思想上更完整,提供的服务也更全,但是做的检测多了,逻辑复杂了,消耗的资源也必定比简单的封装更多。从这个两个模型可以看出事件模型的框架都是:
取得一个合适的时间,用这个时间去poll。然后标记poll之后pending的文件对象。poll出来后判断定时器然后统一处理pending对象
这里绘制一个整体的结构图,不是很规范UML或者其他什么学术的图,只是一个帮助理解的过程:
至此Libev的分析差不多完成了,主要去了解实现的思路。具体如何实现以及从什么样的角度去设计。其结果需要在生产环境中去检验。