事件驱动模型Libev(一)

Libev的作者写了一份很好的官方Manual,比较的齐全,即介绍了Libev的设计思想,也介绍了基本使用还包括内部各类事件详细介绍。这里略微赘述一下。Libev通过一个 ·struct ev_loop· 结结构表示一个事件驱动的框架。在这个框架里面通过ev_xxx结构,ev_initev_xxx_setev_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源代码结构

对于毕业生,尤其是没有接触过一些已有工程代码的新人。拿到一份源码,怎么去熟悉它是首要解决的问题。我一般把会把源码进行分类:一类是产品类的,就比如Redis、Ngnix这一类本身是一个完整的可以运维的成熟产品;另一类就是Libev这样的组件类的。对于组件类的项目,我一般就是分成这样几步:

  1. 有文档看文档,没有文档问相关人员(包括Google),这个组件主要提供什么服务
  2. 结合上述信息使用组件的AIP写个示例程序,跑起来
  3. 大致浏览下源码,分析一下代码的组织结构
  4. 根据使用的API,进到源码中看看主干是怎么样实现的,从而了解整体思路
  5. 再搜刮源码,把一些辅助的功能看下,并在例子中尝试
  6. 之后将整个理解用文字记录下来。提炼两大块内容:实现思想和技巧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等数据结构在后面分析具体监控器是的时候在详细介绍。后面就要跟进程序的逻辑从而了解其设计思想,这样便可以深入的了解一款组件型的开源软件了。

转载:http://my.oschina.net/u/917596/blog/176915

时间: 2024-10-21 21:11:13

事件驱动模型Libev(一)的相关文章

事件驱动模型Libev(二)

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

使用事件驱动模型实现高效稳定的网络服务器程序

使用事件驱动模型实现高效稳定的网络服务器程序 几种网络服务器模型的介绍与比较 围绕如何构建一个高效稳定的网络服务器程序,本文从一个最简单的服务器模型开始,依次介绍了使用多线程的服务器模型.使用非阻塞接口的服务器模型.利用select()接口实现的基于事件驱动的服务器模型,和使用libev事件驱动库的服务器模型.通过比较各个模型,得出事件驱动模型更适合构建高效稳定的网络服务器程序的结论. 前言 事件驱动为广大的程序员所熟悉,其最为人津津乐道的是在图形化界面编程中的应用:事实上,在网络编程中事件驱动

11.python并发入门(part13 了解事件驱动模型))

一.事件驱动模型的引入. 在引入事件驱动模型之前,首先来回顾一下传统的流水线式编程. 开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束 每一个代码块里是完成各种各样事情的代码,但编程者知道代码块A,B,C,D...的执行顺序,唯一能够改变这个流程的是数据.输入不同的数据,根据条件语句判断,流程或许就改为A--->C--->E...--->结束.每一次程序运行顺序或许都不同,但它的控制流程是由输入数据和

由Node.js事件驱动模型引发的思考

引言 近段时间听说了Node.js,很多文章表述这个事件驱动模型多么多么优秀,应用在服务器开发中有很大的优势,本身对此十分感性去,决定深入了解一下,由此也引发了一些对程序设计的思考,记录下来. 什么是Node.js Node.js在官网上是这样定义的:"一个搭建在Chrome JavaScript运行时上的平台,用于构建高速.可伸缩的网络程序.Node.js采用的事件驱动.非阻塞I/O模型使它既轻量又高效,是构建运行在分布式设备上的数据密集型实时程序的完美选择." Node.js的事件

Spring基于事件驱动模型的订阅发布模式代码实例详解

代码下载地址:http://www.zuidaima.com/share/1791499571923968.htm 原文:Spring基于事件驱动模型的订阅发布模式代码实例详解 事件驱动模型简介 事件驱动模型也就是我们常说的观察者,或者发布-订阅模型:理解它的几个关键点: 首先是一种对象间的一对多的关系:最简单的如交通信号灯,信号灯是目标(一方),行人注视着信号灯(多方): 当目标发送改变(发布),观察者(订阅者)就可以接收到改变: 观察者如何处理(如行人如何走,是快走/慢走/不走,目标不会管的

Guava ---- EventBus事件驱动模型

在软件开发过程中, 难免有信息的共享或者对象间的协作. 怎样让对象间信息共享高效, 而且耦合性低. 这是一个难题. 而耦合性高将带来编码改动牵一发而动全身的连锁效应. Spring的风靡正是由于攻克了高耦合问题. 本篇介绍的EventBus中也用到了Spring中的依赖注入. 来进行对象和对象间的解耦(如@Subscribe). Guava解决高耦合採用的是事件驱动模型的思路. 对象能够订阅(subscribe)特定的事件或者公布(publish)特定的事件去被消费. 从以下的代码能够看出, E

YARN中MRAppMaster的事件驱动模型与状态机处理消息过程的分析

在MRv1中,对象之间的作用关系是基于函数调用实现的,当一个对象向另外一个对象传递消息时,会直接采用函数调用的方式,并且这个过程是串行的.比如,当TaskTracker需要执行一个Task的时候,将首先下载Task依赖的文件(JAR包,二进制文件等,字典文件等),然后执行Task.在整个过程中,下载依赖文件是阻塞式的,也就是说,前一个任务未完成文件下载之前,后一个新任务将一直处于等待状态,只有在下载完成之后,才会启动一个独立进程运行该任务.基于函数调用式的编程模型是低效的,它隐含着整个过程是串行

跟我学android之四 事件驱动模型

Android事件驱动模型需要深刻学习和理解,事件驱动模型三要素如下: 事件驱动模型 事件源:事件的制造者,如:按钮 通常会拥有注册和取消监听器的功能 监听器:事件的接收者,通常是自己编写的类的对象 一个实现了事件源所支持的事件接口的类 事件:事件源产生的某一个具体事件 一个事件源可以产生多种事件 一个监听器可以接收多个事件 事件的处理程序通常位于监听器内部 事件驱动模型 工作步骤 1.定义监听器,为每一个事件编写处理方法 2.将监听器对象注册给事件源 3.事件源发生某个事件时调用监听器中对应的

事件驱动模型。。。。有时间弄

public class A{ private Vector aListeners = new Vector(); private int value; public int getValue(){ return value; } public void setValue(int newValue){ if(value!=newValue){ value = newValue; AEvent evt= new AEvent(this,value); //如果值改变的话,就触发事件 fireAEv