9

初入libevent的人,很可能是第一次接触异步编程。Libevent的编程思想,建议还是多看前人的程序,或者是看libevent本身的文档学习。
或者是介绍另外一个库,那就是libuv,它是libev某种意义上的的替代品(而libev又可以算是libevent的某种替代品笑)。libuv的文档我记得也有对异步编程的介绍。好了,这不是本文的内容。

本文地址:https://segmentfault.com/a/1190000005359466

Reference

Programming with Libevent
翻译:Libevent参考手册第一章:设置libevent

序章和知识准备

在文档序章中举了一个例子,介绍了异步编程。这里面列举了几个典型的函数,如下:

头文件
#include <event2/event.h>

结构体
struct event

函数
event_new(), event_free(), event_base_new(), event_add(), event_base_dispatch()

Libevent的基本思路是使用select()。但是使用select很多的话,往往系统开销很大,所以各个UNIX和类UNIX系统是使用了select的替代。但是不同的系统使用的替代各不相同,这导致程序移植起来很麻烦。
Linux:epoll()
BSD:kqueue()
Solaris:/dev/poll, evports

Libevent使用BSD License而不是GPL,这使得它可以很方便地用在一个闭源的项目里面

Libevent的特性

  1. evutil:不同平台的抽象层,主要是网络接口部分
  2. event, event_base:libevent的核心API
  3. bufferevent:缓存化的read/write接口,并且可以与SSL互相封装。

另外还有几个我就不列出了,个人很少用

Libevent的库

  1. libevent_core:Libevent的核心API和功能,包含了上述主要模块
  2. libevent_extra:Libevent提供的额外功能包,这些目前还用不上
  3. libevent:包含上述两个,但是建议以后不要使用

之所以还有event2文件夹,是因为这个文件夹下是新的(也就是目前使用的)libevent库

创建一个event_base

每一个event_base持有和管理多个event,并且判断哪些event是被激活了的。但是这发生在一个单一的线程内。如果你想哟啊在多个线程内并行处理event,则需要在每个线程中各创建一个event_base。

获得一个默认的event_base

struct event_base *event_base_new(void);

这个函数alloc并返回一个带默认配置的event base。

获得一个复杂的event_base

struct event_config *event_config_new(void);
struct event_base *event_base_new_with_config(const struct event_config *cfg);
void event_config_free(struct event_config *cfg);

这几个函数的基本功能就是:获得一个config,配置好后作为event_base创建的参数。用完后记得将config给free掉。

而配置config则需要用到要以下两个函数:

int event_config_require_features(struct event_config *cfg, enum event_method_feature feature);

这里的枚举值有以下几个:
EV_FEATURE_ET:需要边沿触发的I/O
EV_FEATURE_OI:在add和delete event的时候,需要后端(backend)方法
EV_FEATURE_FDS:需要能够支持任意文件描述符的后端方法
上述的值都是mask,可以位与(|)。

int event_config_set_flag(struct event_config *cfg, enum event_base_config_flag flag);

这里的枚举值有以下几个:
EVENT_BASE_FLAG_NOLOCK:event_base不使用lock初始化
EVENT_BASE_FLAG_IGNORE_ENV:挑选backend方法时,不检查EVENT_xxx标志。这个功能慎用
EVENT_BASE_FLAG_STARTUP_IOCP:仅用于Windows。起码我不关心
EVENT_BASE_FLAG_NO_CACHE_TIME:不检查timeout,代之以为每个event loop都准备调用timeout方法。这会导致CPU使用率偏高
EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST:告诉libevent如果使用epoll的话,可以使用基于“changlist”的backend。
EVENT_BASE_FLAG_PRECISE_TIMER:使用更加精确的定时机制

以上两个函数成功时返回0,失败时返回-1;

设置event_base的分离时间

int event_config_set_max_dispatch_interval (
                        struct event_config   *cfg,
                        const struct timerval *mac_interval,
                        int                    max_callbacks,
                        int                    min_priority);

在检查高优先级的event之前,通过限制低优先级调用来防止优先级反转(priority inversion)。而另一个限制条件是max_interval参数,这是表示高优先级事件被调用的最大延时。

成功时返回0,失败时返回-1。默认情况下,`event_base`被设置成默认优先级

检查event_base的backend方法

const char **event_get_supported_methods(void);

返回一个char*数组,说明了libevent支持的所有backend方法。数组以NULL结尾。

读取指定event_base的配置

const char *event_base_get_method (const struct event_base *base);
enum event_method_feature event_base_get_features (const struct event_base *base);

释放一个event_base资源

int event_base_priority_init (structr event_base *base, int n_priorities);

第二个参数表示可支持的优先级,至少为1。0是最优先。这样,event_base中的优先级即为0 ~ nPriorities-1
  最大支持的优先级数为EVENT_MAX_PRIORITIES

int event_base_get_npriorities(struct event_base *base);

上一个函数的读版本

使用事件循环event loop

当event_base和其中的event都配置好了之后,就可以启用libevent了。Event的配置在后文说,这里先讲event loop的运行。

运行loop

int event_base_loop (struct event_base *base, int flags);
int event_base_dispatch (struct event_base *base);

函数的行为是:运行event_base,直到没有event被注册在event_base中位置。这是默认的行为,也就是说,当没有pendingactive事件时,函数返回。
  参数flags可以用来修改默认行为,有以下掩码值:

  1. EVLOOP_ONCE:等待并执行active事件,循环执行到没有事件可执行
  2. EVLOOP_NONBLOCK:循环不等待事件触发,而是即检查即回调
  3. EVLOOP_NO_EXIT:没有事件仍不退出,而是由其他函数触发退出

第二个函数的作用是相同的,等效于使用默认的flags。

停止loop

int event_base_loopexit (struct event_base *base, const struct timeval *tv);
int event_base_loopbreak (struct event_base *base);

第一个函数要求event_base在指定时间后立即停止,如果tv为NULL,则立即停止。但这个函数实际上会使得event_base在执行完全部的callback之后才返回。
  第二个函数的不同之处是使event_base在执行完当前的callback之后,无视其他active事件而立即停止。但需要注意的是,如果当前没有callback,这会导致event_base等到执行完下一个callback之后才退出。

int event_base_got_exit (struct event_base *base);
int event_base_got_break (struct event_base *base);

用来判断是否获得了exit或者是break请求。注意返回值实际上应该是BOOL而不是int

检查event_loop内部的时间

int event_base_gettimeofday_cached (struct event_base *base, struct timeval *tv);
int event_base_update_cache_time (struct event_base *base);

获得event_loop时间。第一个函数获得event_loop最近的时间,比较快,但是不精确。第二个函数可以强制event_loop立即同步更新时间。

将event_base的状态存入文件

void event_base_dump_events (struct event_base *base, FILE *f);

实际上我觉得这个函数能够保存的信息还是不够多。另外,你可以将/dev/stdout句柄打开之后传入也是可以的

在event_base中运行每一个event

typedef int (*event_base_foreach_Event_cb) (const struct event_base *,
                                            const struct event *,
                                            void *);
int event_base_foreach_event (struct event_base *base,
                              event_base_foreach_Event_cb fn,
                              void *arg);

这个函数比较谜。按照资料,这个函数会检查event_base中所有的active或pending的Event,然后调用传入的一个回调,逐个调用。
  注意这里的callback只允许用来检查event的状态,不允许修改。此外,callback的执行时间不应太长,因为会有锁保护。
  为什么说这个函数谜呢?因为我实际上在libevent的头文件中找-不-到这个函数啊!资料里面是有的,但是.h文件中不存在,强行调用,也是找不到函数。从描述来看这个函数其实很好用,也非常适合初学者们跟踪学习event_base及其event的状态。但是……用不了。

使用事件“event”

Libevent的基本处理单元是event,每个event代表一组条件:

  1. 做好read或write准备的文件描述符
  2. 正在准备好read或write准备的文件描述符(边沿触发I/O,没用过,不懂)
  3. 超时
  4. 信号被触发
  5. 用户触发的事件

当一个event被设置好,并且关联到一个event_base里面时,它被称为“initialized”。此时你可以执行add,这使得它进入pending状态。当event被触发或超时时,它的状态称为active,这个情况下对应的callback会被调用。如果event被配置为persist,那么它在callback执行前后都会保持pending的状态。可以通过delete来使得一个event从pending状态重新变成nonpending

创建一个event对象

typedef void (*event_callback_fn) (evutil_socket_t, short, void *);
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。其中fd是文件描述符,需要自行初始化之后再作为参数传入。event_free()释放event的资源。如果event是active或者是pending状态,则函数会将event先变成非active且非pending的状态,然后再释放它。
  参数what表示这个event的需要关注绑定在该fd上的哪些事件。有以下几个掩码值:

  • EV_TIMEOUT:超时
  • EV_READ:有数据可读
  • EV_WRITE:数据可写
  • EV_SIGNAL:系统发出的信号(signal
  • EV_PERSIST:持续事件
  • EV_ET:边沿触发

关于EV_PERSIST

int event_del (struct event *event);
前文所说的add动作的反动作。EV_PERSIST使得一个event不会自动被event_base给delete掉,除非显式地调用这个函数。

使用event自己的ID作为回调参数

回调的时候会传入自定义的arg。但是如果想要让回调能够接受自己的struct event指针作为参数,那么应该使用以下函数:
void *event_self_cbarg();
比如:

struct event *aEvent = event_new (pBase, aFd, 
                                  EV_READ | EV_TIMEOUT, 
                                  aCallback, 
                                  event_self_cbarg());

Note on 2016-11-21: 在最新版本的 libevent 中,这个函数已经被停用了。并且目前并没有这个函数的合适替代。

超时事件

纯超时事件不需要fd(传入-1即可)。Libevent定义了一些方便的用于创建超时事件的宏:

evtimer_new (base, callback, arg)
evtimer_add (ev, tv)
evtimer_del (ev)
evtimer_pending (ev, tv_out)

从测试结果来看,这里的timer event都是一次性执行的,所以请自行在callback中重新add

信号事件

信号时间也不需要fd,但不是传入-1,而是对应的signum

evsignal_new (base, signum, callback, arg)        // 自带EV_PERSIST
evsignal_add (ev, tv)
evsignal_del (ev)
evsignal_pending (ev, what, tv_out)

signal事件的callback是异步的,所以很安全,不像signal()中的回调那样有诸多限制。
注意:不要给signal事件设置timeout

使事件进入pending和non-pending

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

使用优先级的事件

int event_priority_set (struct event *event, int priority);

将事件优先级设置在event base的优先级和0之间。

观察event状态

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

注意这个函数的返回值实际上是BOOL。函数判断event是否为给定的flag(what)而被pending或active。如果tv_out非空,并且EV_TIMEOUT被设置了,那么判断完状态后,event得timeout也会被通过这个参数返回回来。

以下是其他一目了然的get函数:

evutil_short_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);
void event_get_assigement (const struct event  *event,
                           struct event_base  **base_out,
                           evutil_short_t      *fd_out,
                           short               *events_out,
                           event_callback_fn   *callback_out,
                           void                *arg_out);

寻找当前运行中event

struct event *event_base_get_running_event (struct event_base *base);

注意这个函数只支持在event的loop中调用,在其它线程调用的话是不支持的

快速配置一个一次性的事件

int event_base_once (struct event_base *base,
                     evutil_socket_t    fd,
                     short              what,
                     event_callback_fn  cb,
                     void              *arg,
                     const struct timeval *tv);

这个函数完成前面的event_newevent_add动作,并且不返回event对象。调用这个函数创建的对象直接加入到event_base中,并且执行了一次callback之后,自动deletefree掉。
  Event的优先级默认,what不支持EV_SIGNAL和EV_PERSIST。
  从libevent 2.1.2之后,即便一个event还没有被activate过,在event_base被释放时,也会一并被释放掉。在这之前这算是一个bug。不过也请注意其他所有不由libevent管理的资源(比如arg),要防止内存泄漏。

手动激活一个event

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

用指定的标志来激活一个event,慎用该功能,特别是不要再callback中使用。但这是另一个比较谜的函数,因为我在我自己设计的字符设备驱动中使用这个函数,但是实际上我监听着这个设备的event却根本没有被激活。我的调用方式应该是没问题的,难道是还需要内核驱动做什么支持吗?
(2016-7-5 笔记:这里说的可能只是不能在目标event的callback中使用,但是可以在其它无关事件的callback中调用。还没有确认,但是猜测的可能性是:这个函数只有在libevent的loop中调用才能生效,不能异步地在loop外部activate。已经确认了在loop内部是生效的,至于在外部无法生效的原因,是不是我所猜测的那样,目前我还找不到确认的答案)

优化了的公共超时事件

Libevent对add/del超时事件具有 O(logN)的性能,使用与多个不同超时时间的队列。但是如果有多个使用同一个超时值的事件,这样做就显得很低效了。此时libevent使用二进制堆算法(binary heep algorrithm)来完成这种任务。
  此时libevent要涉及一个“common timeout”了:

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

这个函数返回一个持续的timeval指针,指向common timeout。使用这个timeout的事件会被加入到binary heep中,并且只有你指定的特定event_base中有效。返回的timeval指针,可以把timeval值复制出来使用。

系列篇

Libevent官方文档学习笔记(1. libevent_core部分)(本文)
Libevent官方文档学习笔记(2. bufferevent部分)
Libevent官方文档学习笔记(3. evbuffer部分)


amc
927 声望228 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发