libevent 学习笔记 02
潘忠显 / 2020-07-16
本文主要是Libevent概览,重点介绍了库中两个最重要的结构event_base
和event
,也提及到一些为了方便而做的封装。
〇、Libevent概览
设计目标
- 可移植性:支持所有平台上工作
- 高效:使用所在平台上最快的非阻塞IO
- 可扩展性:支持上万的活动套接字
- 便捷:使用Libevent开发快、可移植、稳定
组件
不同的Libevent的头文件对应到不同的组件,主要的几个组件有:
组件名称 | 组件描述 |
---|---|
evutil | 通用功能,抽象出不同平台的网络实现之间的差异 |
event/ event_base |
核心功能,基于不同平台的事件非阻塞IO后端提供了统一的抽象API |
bufferevent | 对event的封装,缓冲套接字的读写,不需要在套接字就绪时立马处理 |
evbuffer | 该模块实现了底层缓冲事件的缓冲,并提供了方便有效访问的功能 |
evhttp | HTTP客户端/服务器的简单实现 |
evdns | DNS客户端/服务器的简单实现 |
evrpc | RPC的简单实现 |
库
构建Libevent时候,会产生几个库。
如果是直接通过configure + make
构建,可以在.lib
目录中找到。
如果 使用Bazel构建 时,会在bazel-bin/copy_libevent/libevent/lib/
目录中找到。
这几个库的名称以及对应的含义:
库名 | 包含的内容 |
---|---|
libevent_core | 所有核心事件和缓冲区功能。该库包含所有event_base 、evbuffer 、bufferevent 和event_util |
libevent_extra | 扩展的协议功能,包括HTTP、DNS和RPC |
libevent | 包含libevent_core和libevent_extra的内容,后边可能被弃用 |
libevent_pthreads | 基于pthreads线程库添加了线程和锁定实现,使用多线程方式的Libevent时需要链接该库 |
libevent_openssl | 提供对使用bufferevents和OpenSSL库的加密通信的支持,使用加密连接需要链接这个库 |
一、event_base
结构
使用Libevent的函数之前,需要分配若干个 event_base
结构,每个 event_base
维护着一个事件集合(a set of events and can poll to determine which events are active)。
如果event_base
设置了使用锁,可以用于多线程。
event_base
会使用“一种方法”来判断哪些事件是否ready。这些方法主要对应不同平台上最高效的异步IO通知方法,包括:select/poll/epoll/kqueue/devpoll/evport/IOCP
默认创建
#include <event2/event.h>
struct event_base *event_base_new(void);
event_base_new()
会检查环境变量,自动选择系统中最快的事件监听方法,创建event_base
结构。
可以指定不使用的方法的两种方式:
- 进程外,通过环境变量,如
EVENT_NOKQUEUE
- 进程内,通过
event_config_avoid_method()
函数
另外还有一些辅助函数可以获得一些关于event_base
的信息:
event_get_supported_methods()
函数可以获得后台支持的方法。event_base_get_method(base)
函数可以获得event_base实际使用的方法。event_base_get_features(base)
函数可以获得支持特性的bitmask。
自定义创建
通过创建并设置 event_config
,再传递给函数 event_base_new_with_config()
,来控制创建何种 event_base。
event_config_new / event_base_new_with_config / event_config_free
通过event_config_set_max_dispatch_interval()
设置检查新事件的时间间隔。
销毁
使用 event_base_free()
可以销毁一个 event_base
,但是不会销毁维护的事件,不会关闭连接,也不会释放指针。
优先级
优先级是event
的属性。但是因为事件都是在event_base
维护,因此event_base
的优先级范围决定了其中事件可以设置什么优先级。
默认情况下,一个event_base
只支持一个优先级。调用 event_base_priority_init
函数,可以让一个event_base支持的优先级范围从 0 到 N-1。0
是最重要的,N - 1
为最不重要的。
事件默认的优先级是 N / 2
,创建之后可以通过 event_priority_set()
设置。
使用 event_base_get_npriorities(base)
获得 event_base 支持的优先级总数N
。
fork()
后需再初始化
通过fork()
创建子进程后,如果还需要使用event_base
,则需要调用 event_reinit()
重新初始化。
debug信息
event_base_dump_events()
函数可以导出event_base
中添加的所有事件及状态,写入到对应的FILE中。
遍历所有的事件并运行一个函数
event_base_foreach_event()
函数可以给event_base
中的每个事件都运行一个函数,类似于std::for_each()
函数
二、event
结构
表示的发生条件
event
是Libevent操作的基础单元,每个event
代表着以下条件发生:
- fd 可读或可写
- fd 变成可读或变成可写(ET)
- 超时
- 信号发生
- 用户触发事件
创建与销毁
使用event_new()
创建事件,使用event_free()
销毁事件。
typedef void (*event_callback_fn)(evutil_socket_t, short, void *);
struct event *event_new( //
struct event_base *base, // 需要使用base来分配和构造event
evutil_socket_t fd, // 非0表示要监控的fd
short what, // 监控fd的事件类型(Flags)
event_callback_fn cb, // 事件变成active后需要运行的回调函数
void *arg // 回调函数的*其他*参数
);
void event_free(struct event *event);
Flags (参数what
的值)
#define EV_TIMEOUT 0x01
#define EV_READ 0x02
#define EV_WRITE 0x04
#define EV_SIGNAL 0x08
#define EV_PERSIST 0x10
#define EV_ET 0x20
上边这种以位来标识实际含义的宏定义方式,通常都是为了几种flag模式可以使用按位或 |
。
EV_PERSIT
标识持久事件(persistent),当active状态调用cb之后,又变成pending状态,而不是non-pending状态。
事件的状态转移
三种状态
- initialized & non-pending
- pending
- active
状态转移图
- 事件构建之后,需要调用
event_add()
函数,将其状态变为 pending 后,才会生效 event_add()
第二个参数是超时时间偏移,而不是时间戳。如果传空,则没有超时- 调用
event_del()
,可以将 pending 状态主动变成 non-pending - pending状态的事件再次调用
event_add()
,会重新计算超时时间,也可以置成NULL
来取消超时 - 调用
event_free()
可以释放non-pending/inactive事件 - 调用
event_free()
也可以释放 pending 或者 active 状态事件,释放前会将其变成 non-pending/inactive - 调用
event_del
函数将一个pending的事件变成non-pending/non-active状态 - 调用
event_del
时,事件已经是active但是还没有执行CB,这种情况下,CB不会再被调用 - 调用
event_remove_timer()
函数可以只移除时间的超时(是这么理解吗???)
持久事件的状态转移
- 非持久事件由 pending 变成 active 后,会在调用cb之前,状态变成non-pending,如果要再变成pending需要调用
event_add()
- 持久事件回调被调用后,仍然是pending状态
- 持久事件上有cb发生,会重计超时,比如一个例子:
If you have an event with flags EV_READ|EV_PERSIST and a timeout of five seconds, the event will become active:
- Whenever the socket is ready for reading.
- Whenever five seconds have passed since the event last became active.
创建特殊事件
为了方便,libevent定义了许多宏,用来处理特殊的事件:
- callback函数格式没变
- 以
evtimer_
开头替换掉event_
前缀的是纯超时事件(定时器) - 以
evsignal_
开头替换掉event_
前缀的是信号事件 - 不要在信号事件上加超时事件
- 除了kqueue之外,只有一个event_base可以监听信号,即使不同信号加入到两个不同的event_base中,也只有一个能收到(v2.1.12)
使用这些宏的好处,可以简化开发,也能使代码更清晰、可读性更好。
#define evtimer_new(base, callback, arg) \
event_new((base), -1, 0, (callback), (arg))
#define evsignal_new(base, signum, cb, arg) \
event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)
Note that signal callbacks are run in the event loop after the signal occurs, so it is safe for them to call functions that you are not supposed to call from a regular POSIX signal handler.
一次性事件可以通过调用event_base_once()
函数创建,与event_new()
类似
- 不同于
event_new()
之处在于,不会返回event指针,而是返回整数,表示是否创建成功。 - 如果事件不触发callback,会一直占用内存,直到
event_base_free()
- 传给callback的arg指针指向的对象不会自动释放
使用事件本身作为callback参数
上边event_new()
的定义中的arg
是用于当事件变成active时,传递给callback的参数。
调用event_self_cbarg()
可以得到神奇的事件本身的指针,会告诉event_new()
将自己作为CB的参数传入。换句话说:将event_new()
的输出作为自己的输入。
用途举例:在CB中销毁自己。
事件优先级
多个优先级的多个事件同时变为active时:
- Libevent运行高优先级事件(callback),然后再次检查事件;
- 仅当没有active的高优先级事件时,低优先级事件才会运行。
在介绍 event_base
的时候有提到,一个event_base是可以设置优先级范围,针对一个事件则可以统调用 event_priority_set()
函数,来给一个事件指定一个在event_base优先级范围内的优先级。
event_base_priority_init(base, 2);
/* Now base has priority 0, and priority 1 */
struct event* important = event_new(base, fd, EV_WRITE|EV_PERSIST, write_cb, NULL);
struct event* unimportant = event_new(base, fd, EV_READ|EV_PERSIST, read_cb, NULL);
event_priority_set(important, 0);
event_priority_set(unimportant, 1)
获取事件属性
event_get_
开头的函数,可以获得事件属性。
- 单个属性获得可以
event_get_
加:signal
、fd
、base
、events
、callback
、callback_arg
、priority
。 event_get_events()
返回的是事件的flag,对应创建时的what
参数event_get_assignment()
可以获得事件的所有属性分配地址
上边有提到event_get_base()
可以根据事件找到base,而event_base_get_running_event()
可以根据base找到其维护事件。
struct event *event_base_get_running_event(struct event_base *base);
其他
-
调用
event_active()
主动激活还达到条件的事件 -
base根据不同的应用场景选择超时优化
- 随机的超时时间选择默认的二叉堆,
O(lg n)
- 相同超时时间,选择双向链表,
O(1)
- 堆和链表混合使用:[双向链表]调用
event_base_init_common_timeout()
返回timeval指针并设置为大部分相同超时值(另外一个timeval的指针),[二叉堆]其他的使用普通的timeval变量去设置
- 随机的超时时间选择默认的二叉堆,
-
调用
event_initialized()
判断一个事件指针是否已经初始化。对随机内存不可靠,如果在创建时调用memset()
或bzero()
将内存设置为0了,后便可以使用该函数判断。
三、循环
event_base 循环
event_base_loop
就相当于一个for循环,循环等待事件发生并调用其callback。直到所有的事件都处理完成/无监听的事件,或通过event_base_loopexit()
等方式显式退出。
int event_base_loop(struct event_base *base, int flags);
如果什么flags都不设置,即flags=0的情况下, 循环过程中重复检查是否有注册的事件被触发,就回将时间变成active状态,调用其回调函数。 循环的event_base中没有注册事件了,就停止,继续往下走。
特殊标签
#define EVLOOP_ONCE 0x01
#define EVLOOP_NONBLOCK 0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04
EVLOOP_ONCE
: 循环将等待,直到某些事件变为活动状态,然后运行活动事件,直到没有其他要运行的状态,然后返回EVLOOP_NONBLOCK
: 循环将不等待事件触发:它将仅检查是否有任何事件准备立即触发,并在可能时运行其回调EVLOOP_NO_EXIT_ON_EMPTY
: 一旦没有挂起或活动事件,循环将立即退出
event_base_dispatch
大部分情况下,会使用到flags=0
设置的event_base_loop()
,为了方便,直接定义了一个event_base_dispatch()
。
// 是直接 define 定义的吗?
exit/break/continue
通常是检查所有事件,然后运行其中激活事件中具有最高优先级的事件,然后再重复这一过程。
event_base_loopbreak()
是等待完成当前处理的事件完成之后立马退出event_base_loopexit()
是等待所有active的事件都结束才会退出event_base_loopexit()
可以指定时间后退出。event_base_loopcontinue()
可以在停止本次循环,继续(新一轮)扫描。
时间缓存
event_base_gettimeofday_cached()
可以在callback中获得callback在开始调用时缓存的时间- 如果callback很短,可以替换
gettimeofday()
,避免使用系统调用 - 如果callback时间很长,这个值将很不精确,可以使用
event_base_update_cache_time()
来更新一下缓存的时间