事件库之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;
IO监控器
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*/
这里首先只用关注一个 “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或则其他文件复用机制修改系统监控的条件。这里我们把这两个主要的物理结构梳理下:
总结一下注册过程就是通过之前设置了监控条件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”的优先级模型。