event

概念

Libevent的基本操作单元就是event。每一个事件都代表一列情况:
1.文件描述符就绪
2.时间器就绪
3.信号就绪
4.一个用户触发的event
这几个事件有相同的生命周期,一旦通过调用event_new 设置 event 并与 event_base相关联后,event 事件就会被初始化。初始化后,当我们使用这个event的时候,需要先把它注册到event_base中,通过event_add 将 该event注册为 pendding态(未决态)。
如果这个event绑定的事件就绪的话,event就会被激活。接着它的回调函数就会运行。如果没设置 persistent(持久),则执行完一次回调函数后,该event就会是 非 pendding态,也就是说该事件只会被激活一次。(活跃后调用其回调函数视为激活)
如果设置了 persistent 选项,则它会一直为pendding态,一旦再次就绪,其回调函数就会被再次调用。

修改event的pendding态
event_add  设置event为 pendding态
event_del  设置event为 non-pendding态

申请并初始化event

下面函数出自于 2.0.1 , cb 类型出自于 2.0.4
#define EV_TIMEOUT      0x01 
//第一次通过event_new申请event的时候会忽略EV_TIMEOUT标志
//只有当我们event_add 的时候设置了time_out参数,它才会被设置
//不过这个标志也不是我们设置的,而是当事件超时后Libevent为我们设置的
//我们可以在回调函数中查看这个标志对于超时event
#define EV_READ         0x02  //设置关注可读事件
#define EV_WRITE        0x04//设置关注可写事件
#define EV_SIGNAL       0x08  //设置关注信号事件
#define EV_PERSIST      0x10  
//设置持久,持久就是即使跑了回调函数event还是pendding的
#define EV_ET           0x20  //设置ET 为了epoll 2.0.1 版本

typedef void (*event_callback_fn)(evutil_socket_t fd, short what, void * arg);

struct event *event_new(struct event_base *base, evutil_socket_t fd,
    short what, event_callback_fn cb,
    void *arg);

void event_free(struct event *event);

  初始化event这个概念特别重要,申请指的是申请event需要的内存,我们可以自己从堆上或者栈上申请event空间,但是这并不代表event会被初始为相应的属性。为了给他添加属性我们必须调用event_assign 函数给一个已分配内存的event初始化相应的属性,或者我们直接调用event_new 申请和初始化一起完成。(event_assign 后面有讲到)
  event_new 会申请并初始化一个 event 节点,what 参数是上面的一系列flag, fd 为一个正数的文件描述符,what 为选项,arg 是额外的参数。如果fd是一个文件,则我们只关注读或者写事件,当调用失败时返回NULL。
  当event变成活跃的,Libevent会调用该event相对应的回调函数,它会给回调函数传入以下参数:

  1. fd文件描述符
  2. arg
  3. what参数

  what参数很重要,它代表了Libevent想告诉我们到底是EV_READ 还是EV_WRITE 还是 EV_TIMEOUT触发的事件
  新的event都是 非注册过的(non-pending),我们需要通过调用event_add来注册使其成为注册过的未决态,event_del去删除这个pending态。如果我们想删除该event对象,可以通过 event_free来释放。对于一个处于pending 或者 active 态的event去调用这个释放函数是安全的,它会使相应的event在释放前先把它变成非pending 和 非 active态,然后去释放它。对于多个event去关注同一个fd ,如果这个fd就绪了,这些回调函数的调用顺序是未定义的。

Persistence

  持久化,默认event是不持久化的,默认的event调用完 回调后,event就会立即变为 non-pendding状态,不再专注事件,除非在回调函数内再次通过 event_add 注册该事件。如果我们设置了EV_PERSIST , 使 event 持久化的话,那么它一直都是pendding状态。
  如果我们设置了EV_PERSIST选项,但又不想再关注该事件,则在调用event_del删除该事件,使其状态 成为 non-pendding即可。

当持久化遇上超时

  因为我们初始化event的时候,EV_TIMEOUT 会忽略,所以得后期通过 event_add来设置。但是当EV_PERSISIT遇到timeout后,每次回调函数执行完后,timeout就会被重置为了下一轮的超时作准备。当timeout 与 其他关心标志组合到一起后,对于event的活跃判定如下:
1. 关心的事件就绪了
2. timeout超时了
  注意如果我们tv的值设为0,持久化的时候会失败,在 libevent 2.0.21 遇到的bug

#include <event2/event.h>

static int n_calls = 0;

void cb_func(evutil_socket_t fd, short what, void *arg)
{
    struct event *me = arg;
    printf("cb_func called %d times so far.\n", ++n_calls);

    if (n_calls > 100)
       event_del(me);
}

void run(struct event_base *base)
{
    struct timeval one_sec = { 1, 0 };
    struct event *ev;
    /* We're going to set up a repeating timer to get called called 100
       times. */
    ev = event_new(base, -1, EV_PERSIST, cb_func, event_self_cbarg());
    event_add(ev, &one_sec);
    event_base_dispatch(base);
}

给自己回调函数如何传递event过去

void *event_self_cbarg()

  我们知道,给回调函数传参是通过 event_new 这个函数来传递的,它申请event的同时,注册回调函数,调用时给回调函数传参,但是我们无法在调event_new的时候,传递一个event自己的地址,因为那个时候event还未申请,所以通过event_self_cbarg()返回对应的一个magic 数,通过这个magic数,Libevent就知道传参的时候就把对应的event传递过去即可,注意这个函数只存在 2.1.1 版本

#include <event2/event.h>

static int n_calls = 0;

void cb_func(evutil_socket_t fd, short what, void *arg)
{
    struct event *me = arg;

    printf("cb_func called %d times so far.\n", ++n_calls);

    if (n_calls > 100)
       event_del(me);
}

void run(struct event_base *base)
{
    struct timeval one_sec = { 1, 0 };
    struct event *ev;
    /* We're going to set up a repeating timer to get called called 100
       times. */
    ev = event_new(base, -1, EV_PERSIST, cb_func, event_self_cbarg());
    event_add(ev, &one_sec);
    event_base_dispatch(base);
}

timeout 相关的宏

  有一系列以evtimer_开头的宏,这些宏可以用来申请或者操作关于timeout类型的event,所以这些宏很便利,但是这会使代码变得不清晰,有点难以阅读。
  从下面的宏函数可以看出来,默认使用的宏开始并没有设置持久化,就只是单单的一个超时事件

#define evtimer_new(base, callback, arg) \
event_new((base), -1, 0, (callback), (arg))
#define evtimer_add(ev, tv) \
event_add((ev),(tv))
#define evtimer_del(ev) \
event_del(ev)
#define evtimer_pending(ev, tv_out) \
event_pending((ev), EV_TIMEOUT, (tv_out))

Signal event

  Libevent也支持Posix标准的信号事件,当信号发生后,该处理函数会在eventloop中被调用。但是注意不要把一个timeout与信号联合起来,这样也许不支持
  从下面宏函数中可以看到,signal申请event的宏函数和 timeout申请event的宏函数还是有所不一样的,signal增加了 PERSIST标志,而timeout却没有

v 2.0.1
#define evsignal_new(base, signum, cb, arg) \
event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)
#define evsignal_add(ev, tv) \
event_add((ev),(tv))
#define evsignal_del(ev) \
event_del(ev)
#define evsignal_pending(ev, what, tv_out) \
event_pending((ev), (what), (tv_out))
Signal事件的限制

对于当前Libevent来讲,对于大多数backend method (指该event_base使用的method),在同一时间下,每一个进程只能有一个event_base去监听信号事件,也就是多线程多event_base的情况下,只有一个线程能够监听信号事件。如果你增加俩个不同的信号或者相同的信号在俩个不同的event_base中,也只有一个event_base可以监听,kqueue方法不受该限制

论event的申请方式

  有些人喜欢申请一堆event,把它们放入一个结构体中,当做这个结构体的一部分,这比单独一个个的使用event来讲,它有如下的优点

  1. 防止堆上小块内存的申请
  2. 减少了当一个指针由一个event指向另一个event的开销
  3. 增加了缓存局部性,增加了cache命中的几率

  虽然有上么这些优点,但是这个用法会打破二进制的兼容性对于 Libevent来讲,因为很可能不同版本的Libevent的event大小不同。上面这三点都是很小的成本,对于大多数app来讲都是没必要的。我们应该坚持去使用event_new来申请event,除非你发现一使用event_new的方式已经给你的性能上造成了影响(对于堆上),那么你可以使用event_assign函数来提高性能
  注意这个函数会造成二进制不兼容问题,可能导致非常难排查到的bug,也需要注意不能使用已经一个 pending 来调用,如果已经pending ,必须先event_del 然后再调用

event参数是一个未初始化的event结构体的地址
成功0  失败-1
int event_assign(struct event *event, struct event_base *base,
evutil_socket_t fd, short what,
void (*callback)(evutil_socket_t, short, void *), void *arg);
栈上申请event的列子
#include <event2/event.h>
/* Watch out! Including event_struct.h means that your code will not
* be binary-compatible with future versions of Libevent. */
#include <event2/event_struct.h>
#include <stdlib.h>
struct event_pair {
evutil_socket_t fd;
struct event read_event;
struct event write_event;
};
void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);
struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{
struct event_pair *p = malloc(sizeof(struct event_pair));
if (!p) return NULL;
p->fd = fd;
event_assign(&p->read_event, base, fd, EV_READ|EV_PERSIST, readcb, p);
event_assign(&p->write_event, base, fd, EV_WRITE|EV_PERSIST, writecb, p);
return p;
}
使用event_assign时的二进制兼容问题

   因为可能不同的Libevent的event的大小不同,那么我们直接使用下面的这种方式申请,那么很可能造成版本不兼容问题。因为可能我们这个版本写的网络程序会跟另一个版本通信,那么这种方式势必会造成BUG,如下所示:

代码列子
struct Encry {
event  my_event
int  scret_key;
};
void  output(struct Encry * PEncry)
{
      EncodeWrite(PEcry);
}
bool Dec(struct Encry * PEncry)
{
      return  Decode(PEncry->scret_key);
}

  我们可以分析下这份代码看上去没什么BUG,但是如果我们把它写成一份网络通信的代码,那么这个时候就可能有BUG了。假设机器A使用Libevent A版本,机器B使用Libevent B版本,假设这俩个机器环境一致。那么从A输出的二进制文件再到B机器解析,这个时候就发生错误了,试想A版本的event 有20字节,B版本的event有10字节,直接这样去解析会造成错误,因为B索引的scret_key是原来A中的event中的内容,势必解析失败

获得当前event结构体大小
size_t event_get_struct_event_size(void)

使 event 处于 pending

int event_add(struct event *ev, const struct timeval *tv)
int event_del(struct event *ev)
int event_remove_timer(struct event *ev)

  调用event_new 只是申请了一个基于某一个event_base的event事件,它并不会有相应的回调函数被调用,因为它处于非预备态(non-pending)。如果我们想让它生效,就得调用event_add使其处于pending态并生效。如果我们对一个 pending 态的event调用这个函数,如果添加了timeval参数,那么这个event的timeval参数会被刷新。如果没有添加那么什么影响都没有
  调用event_del会使一个 active 或者 pending 状态的event处于非pending状态,对于非pending调用没有任何影响。当我们对一个active状态的event调用,它的回调函数如果没有被执行,那么它的回调函数将取消执行,并且event处于non-pending状态

删除timeout属性
v 2.1.2
int event_remove_timer(struct event *ev);

  调用上面这个函数可以消除当前event的超时属性,这就意味着如果该event还有其他的属性比如I/O或者信号,那么只删除超时的属性。如果这个event只有超时属性,那么相当于event_del。

Events with priorities

  因为Libevent并没有规定同一时间下同时活跃的多个event的调用顺序,所以我们可以通过优先级来实现这一点。默认如果一个event_base开启了优先级,如果没有调用下面这个函数来设置event的优先级,那么event的优先级默认是event_base设置优先级时的n_priority参数值的一半

成功 0  失败 -1  Libevent 1.0
int event_priority_set(struct event *event, int priority)

  调用这个函数必须在event_new 或者 event_assign之后调用 (event初始化),在event_add之前。Libevent每次执行的逻辑判断是先执行完所有最高优先级的event的回调函数,执行完毕后再次检测,如果还有最高优先级的event活跃就继续执行,这样一直到最高的调用完毕才会去调用更低优先级的event的回调函数

#include <event2/event.h>
void read_cb(evutil_socket_t, short, void *);
void write_cb(evutil_socket_t, short, void *);
void main_loop(evutil_socket_t fd)
{
struct event *important, *unimportant;
struct event_base *base;
base = event_base_new();
event_base_priority_init(base, 2);
/* Now base has priority 0, and priority 1 */
important = event_new(base, fd, EV_WRITE|EV_PERSIST, write_cb, NULL);
unimportant = event_new(base, fd, EV_READ|EV_PERSIST, read_cb, NULL);
event_priority_set(important, 0);
event_priority_set(unimportant, 1);
/* Now, whenever the fd is ready for writing, the write callback will
happen before the read callback. The read callback won’t happen at
all until the write callback is no longer active. */
}

获取event的状态或属性

 int event_pending(const struct event *ev, short what, struct timeval *tv_out)

  这个函数返回是否当前event是否为pending 或者 active 状态,如果是的pending的话,会返回对应event的flag值,并且如果当前event 设置了timeout属性并且event_add的时候也传递了tv参数那么tv_out就会被设置为event在即将超时前所保存的时间。那如果不是的话返回0 也就是false。

int event_get_signal(ev)
evutil_socket_t event_get_fd(const struct event *ev)
struct event_base *event_get_base(const struct event *ev)
short event_get_events(const struct event *ev)
event_callback_fn event_get_callback(const struct event *ev)
void *event_get_callback_arg(const struct event *ev)
int event_get_priority(const struct event *ev)只有它是 v2.1.2

  这四个函数从命名与参数上就能看出返回的是与event相关联的一些属性

void event_get_assignment(const struct event *event,
struct event_base **base_out,
evutil_socket_t *fd_out,
short *events_out,
event_callback_fn *callback_out,
void **arg_out)

  这个函数以输出型参数的方式返回了相应event的属性值,如果某个参数传递为NULL,该属性就会被忽略

一次性的具有RAII特性的event
int event_base_once(struct event_base *, evutil_socket_t, short,
void (*)(evutil_socket_t, short, void *), void *, const struct timeval *)

  这个函数申请的event具有RAII的特性,也就是我们不需要自己去event_free 它,它会自动释放。它跟event_new 很像但是它不支持 EV_SIGNAL or EV_PERSIST,这个申请的event只能以默认的优先级运行。通过这个接口申请的event我们是不能自己通过event_del 把它删除的或者 手动激活的,如果我们想要去取消这个event可以通过创建一个相似的常规的event。
  最后,当这个特殊的event的callback函数被回调后,Libevent会自动释放event的内存,我们不需要管。但是如果一直不活跃,在Libevent 2.0 中 这个特殊的event的内存会一直存在即是event_base都被释放过了,在Libevent 2.1.2 中这些特殊的不活跃的event会随着event_base的释放而一起释放掉。需要注意的是,如果arg参数是一段我们自己申请的内存,那么Libevent是不会帮我们释放的,需要我们自己去释放。

主动激活

  在极少的情况下这个event的条件未就绪我们也能主动激活它,通过如下函数

void event_active(struct event *ev, int what, short ncalls)

  这个what 是一个EV_READ/EV_WRITE/EV_TIMEOUT的组合(1个或多个的随机组合)的flag,并且这个event不必事先就处于pending态,主要注意的是不要在callback函数或者其他地方对同一个event递归的调用 event_active函数,因为这样可能造成死循环

struct event *ev;
static void cb(int sock, short which, void *arg) {
/* Whoops: Calling event_active on the same event unconditionally
from within its callback means that no other events might not get
run! */
event_active(ev, EV_WRITE, 0);// 这里用进行了激活会造成无限递归
}
int main(int argc, char **argv) {
struct event_base *base = event_base_new();
ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);
event_add(ev, NULL);
event_active(ev, EV_WRITE, 0);
event_base_loop(base, 0);
return 0;
}

当具有多个相同的timeout时的最优化

  Libevent默认使用二叉堆来管理timeout的值,所以增删查改都是log(n)。如果我们所有的超时事件的tv值完全是随机的话,用二叉堆是最优的(其实Libevent可以考虑使用hash来管理),但是如果你有多个相同的tv值的超时事件的话二叉堆就不是最优的了,所以 Libevent提供了一个双端队列,因为双端队列的头插头删尾插尾删都是O(1),所以用这个双端队列管理相同tv值的超时事件时它的效率是高于二叉堆的增删都是O(1),但是同它管理随机的tv值的时候因为要进行大小排序,把快超时的排前面增加肯定是O(N)了,下面这个是Libevent提供的队列接口

const struct timeval *event_base_init_common_timeout(
struct event_base *base, const struct timeval *duration);

  它的返回值是一个特殊的timeval结构体的地址,我们不必去关心它返回的具体的内容,这个特殊的结构体只是用来给Libevent表示,我要使用O(1)的方式插入值为 duation的timeout事件。
  这个特殊的结构体可以用来自由的被拷贝或者赋值,对于这个特殊的结构体它只针对于你使用的对应的event_base有效。

#include <event2/event.h>
#include <string.h>

struct timeval ten_seconds = { 10, 0 };
void initialize_timeout(struct event_base *base)
{
   struct timeval tv_in = { 10, 0 };
   const struct timeval *tv_out;
   tv_out = event_base_init_common_timeout(base, &tv_in);
   memcpy(&ten_seconds, tv_out, sizeof(struct timeval));
}
int my_event_add(struct event *ev, const struct timeval *tv)
{
/* Note that ev must have the same event_base that we passed to
initialize_timeout,注意event必须是有相同的调用initialize_timeout后的
event_base */
if (tv && tv->tv_sec == 10 && tv->tv_usec == 0)
   return event_add(ev, &ten_seconds); 
else
   return event_add(ev, tv);
}
//注意我们想要O(1)添加时使用的是相应的特殊timeval的值

  跟上面所有优化一样,少用为妙,并且它的版本在 v 2.0.4

检测event是否初始化

  下面这个接口可以用来识别这个event是否是已经被event_new/event_assign 初始化过的,但是通常上不要使用。除非你对你程序拿捏的很准,你能很好的区分或者管理内存。你不能去调这个函数在一个未初始化的内存或者非法内存上可能会造成严重错误。这里的初始化内存对Libevent来说就是合法内存且全部内容为0(即通过calloc 或者 bzero / memset初始化过的内存)

int event_initialized(const struct event *ev);
#define evsignal_initialized(ev) event_initialized(ev)
#define evtimer_initialized(ev) event_initialized(ev)
#include <event2/event.h>
#include <stdlib.h>
struct reader {
evutil_socket_t fd;
};
#define READER_ACTUAL_SIZE() \
(sizeof(struct reader) + \
event_get_struct_event_size())
#define READER_EVENT_PTR(r) \
((struct event *) (((char*)(r))+sizeof(struct reader)))
//其实就是P向下偏移fd大小的字节,返回指向event的地址
struct reader *allocate_reader(evutil_socket_t fd)
{
     struct reader *r = calloc(1, READER_ACTUAL_SIZE());
     if (r)
        r->fd = fd;
     return r;
}
void readcb(evutil_socket_t, short, void *);
int add_reader(struct reader *r, struct event_base *b)
{
struct event *ev = READER_EVENT_PTR(r);
if (!event_initialized(ev))
    event_assign(ev, b, r->fd, EV_READ, readcb, r);
return event_add(ev, NULL);
}
旧的event接口
void event_set(struct event *event, evutil_socket_t fd, short what,
void(*callback)(evutil_socket_t, short, void *), void *arg);
int event_base_set(struct event_base *base, struct event *event);

   第一个函数跟event_assign一样都是在已有的内存上初始化event,只不过初始化后的event跟 current_base 相关联。第二个函数是用来改变关联关系的,如果你初始化完后,想指定一个event_base跟它相关联那么可以接着调用第二个函数来完成这种转化。

current_base

  旧版本有个current_base 的概念,其实这个就是一个全局变量的event_base,通过event_init来申请一个全局变量的event_base,然后剩下的其他操作都是直接附加到了这个current_base上,现在也已经弃用了。

阅读更多

没有更多推荐了,返回首页