`
xidajiancun
  • 浏览: 455567 次
文章分类
社区版块
存档分类
最新评论

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的堆的内存管理也是通过这样的关系的。具体这里堆的实现,感兴趣的可以仔细看下实现。这里的操作就是将这个时间权重放到堆中合适的位置。这里堆单元的结构为:

1 typedef struct {
2 ev_tstamp at;
3 WT w;
4 } ANHE;

其实质就是一个时刻at上挂一个放定时器watcher的list。当超时时会依次执行这些定时器watcher上的触发回调函数。

1.4定时器监控器的触发

最后看下在一个事件驱动器循环中是如何处理定时器监控器的。这里我们依然抛开其他的部分,只找定时器相关的看。在“/ calculate blocking time /”块里面,我们看到计算blocking time的时候会先:

1 if (timercnt) {
2 ev_tstamp to = ANHE_at (timers [HEAP0]) - mn_now;
3 if (waittime > to) waittime = to;
4 }

如果有定时器,那么就从定时器堆(一个最小堆)timers中取得堆顶上最小的一个时间。这样就保证了在这个时间前可以从backend_poll中出来。出来后执行timers_reify处理将pengding的定时器。

timers_reify中依次取最小堆的堆顶,如果其上的ANHE.at小于当前时间,表示该定时器watcher超时了,那么将其压入一个数组中,由于在实际执行pendings二维数组上对应优先级上的watcher是从尾往头方向的,因此这里先用一个数组依时间先后次存下到一个中间数组loop->rfeeds中。然后将其逆序调用ev_invoke_pending插入到pendings二维数组中。这样在执行pending事件的触发动作的时候就可以保证,时间靠前的定时器优先执行。函数 feed_reversefeed_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将信号监控器注册到驱动器上

这里首先介绍一个数据结构:

1 typedef struct
2 {
3 EV_ATOMIC_T pending;
4 EV_P;
5 WL head;
6 } ANSIG;
7 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就是用这样的方式来管理信号的。 这里的代码用宏控制了。其逻辑大体是这样的

1 #if EV_USE_SIGNALFD
2 res = invoke_signalfd
3 # if EV_USE_SIGNALFD
4 if (res is not valied)
5 # endif
6 {
7 use evpipe to instead
8 }

这个是框架。其具体的实现可以参考使用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。

1 ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ); /* for signalfd */
2 ev_set_priority (&sigfd_w, EV_MAXPRI);
3 ev_io_start (EV_A_ &sigfd_w);

也就是注册了sigfdcb函数。该函数:

1 ssize_t res = read (sigfd, si, sizeof (si));
2 for (sip = si; (char *)sip < (char *)si + res; ++sip)
3 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则是:

1 ev_init (&pipe_w, pipecb);
2 ev_set_priority (&pipe_w, EV_MAXPRI);
3 ev_io_set (&pipe_w, evpipe [0] < 0 ? evpipe [1] : evpipe [0], EV_READ);
4 ev_io_start (EV_A_ &pipe_w);

pipe_w是驱动器自身的loop->pipe_w。并为其设置了回调函数pipecb:

01 #if EV_USE_EVENTFD
02 if (evpipe [0] < 0)
03 {
04 uint64_t counter;
05 read (evpipe [1], &counter, sizeof (uint64_t));
06 }
07 else
08 #endif
09 {
10 char dummy[4];
11 read (evpipe [0], &dummy, sizeof (dummy));
12
13 }
14 ...
15 xxx
16 ...
17 for (i = EV_NSIG - 1; i--; )
18 if (expect_false (signals [i].pending))
19 ev_feed_signal_event (EV_A_ i + 1);

这里将上面的技巧#if XXX if() #endif {}拓展为了#if XXX if() {} else #endif {} 。这里和上面的操作其实是一样的。后续操作和signalfd里面一样,就是读光pipe里面的内容,然后依次将watcher加入到pendings数组中。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics