Libevent总结
一、Libevent简介
Libevent是开源社区一款高性能的I/O框架库,其具有如下特点:
1、跨平台支持。Libevent支持Linux、UNIX和Windows。
2、统一事件源。libevent对i/o事件、信号和定时事件提供统一的处理。
3、线程安全。libevent使用libevent_pthreads库来提供线程安全支持。
4、基于reactor模式的实现。
5、轻量级,专注于网络,没有ACE那么臃肿庞大
6、可以注册事件优先级
二、Reactor 模式
2.1 Reactor简介
首先来回想一下普通函数调用的机制:程序调用某函数->函数执行,程序等待->函数将结果和控制权返回给程->程序继续处理。
Reactor 释义“反应堆”,是一种事件驱动机制。和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰相反,Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上,如果相应的事件发生,Reactor 将主动调用应用程序注册的接口,这些接口又称为“回调函数”。使用 Libevent 也是向 Libevent 框架注册相应的事件和回调函数;当这些事件发生时,Libevent 会调用这些回调函数处理相应的事件(I/O 读写、定时和信号)。
2.2 Reactor 模式的优点
Reactor 模式是编写高性能网络服务器的必备技术之一,它具有如下的优点:
1)响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的;
2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/ 进程的切换开销;
3)可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;
4)可复用性,reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;
2.3 Reactor 模式框架
使用 Reactor 模型,必备的几个组件:事件源(描述符)、Reactor 框架、多路复用机制和事件处理程序,先来看看 Reactor 模型的整体框架,接下来再对每个组件做逐一说明。
1)事件源(handle)
由操作系统提供,用于识别每一个事件,如Socket描述符、文件描述符等。在Linux中,它用一个整数来表示。事件可以来自外部,如来自客户端的连接请求、数据等。事件也可以来自内部,如定时器事件。
2)event demultiplexer——事件多路分发机制(同步事件分离器)
由操作系统提供的 I/O 多路复用机制,比如 select 和 epoll。 程序首先将其关心的句柄(事件源)及其事件注册到 event demultiplexer 上; 当有事件到达时,event demultiplexer 会发出通知“在已经注册的句柄集中,一个或多 个句柄的事件已经就绪”; 程序收到通知后,就可以在非阻塞的情况下对事件进行处理了。 对应到 libevent 中,依然是 select、poll、epoll 等,但是 libevent 使用结构体 eventop 进行了 封装,以统一的接口来支持这些 I/O 多路复用机制,达到了对外隐藏底层系统机制的目的。
3)Reactor——反应器(管理器)
定义了一些接口,用于应用程序控制事件调度,以及应用程序注册、删除事件处理器和相关的描述符。它是事件处理器的调度核心。 Reactor管理器使用同步事件分离器来等待事件的发生。一旦事件发生,Reactor管理器先是分离每个事件,然后调度事件处理器,最后调用相关的模板函数来处理这个事件。
4) Event Handler——事件处理程序(事件处理器接口)
事件处理程序提供了一组接口,每个接口对应了一种类型的事件,供 Reactor 在相应的事件发生时调用,执行相应的事件处理。通常它会绑定一个有效的句柄。 对应到 libevent 中,就是 event 结构体。
5)具体的事件处理器
是事件处理器接口的实现。它实现了应用程序提供的某个服务。每个具体的事件处理器总和一个描述符相关。它使用描述符来识别事件、识别应用程序提供的服务。
2.4 Reactor 事件处理流程
三、libevent库的使用
3.1 使用步骤
1、调用event_init函数创建event_base对象。一个event_base相当于一个reactor实例。
2、创建具体的事件处理器,并设置它们所从属的reactor实例。evsignal_new和evtimer_new分别用于创建信号事件处理器和定时事件处理器,它们的统一入口是event_new函数,event_new函数成功时返回一个event类型的对象,也就是libevent的事件处理器
3、调用event_add函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应的事件添加到事件多路分发器中。
4、调用event_base_dispatch函数来执行事件循环。
5、事件循环结束后,使用*_free系列函数来释放系统资源。
3.2 事件处理流程
当应用程序向 libevent 注册一个事件后,libevent 内部是怎么样进行处理的呢?
下面的图就给出了这一基本流程。
1)首先应用程序准备并初始化 event,设置好事件类型和回调函数;这对应于前面第步骤 2 和 3;
2)向 libevent 添加该事件 event。对于定时事件,libevent 使用一个小根堆管理,key 为超 时时间;对于 Signal 和 I/O 事件,libevent 将其放入到等待链表(wait list)中,这是一 个双向链表结构;
3)程序调用 event_base_dispatch()系列函数进入无限循环,等待事件,以 select()函数为例; 每次循环前 libevent 会检查定时事件的最小超时时间 tv,根据 tv 设置 select()的最大等待时间,以便于后面及时处理超时事件; 当 select()返回后,首先检查超时事件,然后检查 I/O 事件;
四、libevent 源代码文件组织
4.1 源代码组织结构
1)头文件
主要就是 event.h:事件宏定义、接口函数声明,主要结构体 event 的声明;2)内部头文件 xxx-internal.h:内部数据结构和函数,对外不可见,以达到信息隐藏的目的;
3) libevent 框架
event.c: event 整体框架的代码实现;
4)对系统 I/O 多路复用机制的封装
epoll.c:对 epoll 的封装;
select.c:对 select 的封装;
devpoll.c:对 dev/poll 的封装;
kqueue.c:对 kqueue 的封装;
5)定时事件管理
min-heap.h:其实就是一个以时间作为 key 的小根堆结构;
6)信号管理
signal.c:对信号事件的处理;
7)辅助功能函数
evutil.h 和 evutil.c:一些辅助功能函数,包括创建 socket pair 和一些时间操作函数:加、减和比较等。
8)日志
log.h 和 log.c: log 日志函数
9)缓冲区管理
evbuffer.c 和 buffer.c: libevent 对缓冲区的封装;
10)基本数据结构
compat\sys 下的两个源文件: queue.h 是 libevent 基本数据结构的实现,包括链表,双向链表,队列等; _libevent_time.h:一些用于时间操作的结构体定义、函数和宏定义;
11)实用网络库
http 和 evdns:是基于 libevent 实现的 http 服务器和异步 dns 查询库
五、libevent 的核心
5.1 ibevent 的核心---event
Libevent 是基于事件驱动(event-driven)的,从名字也可以看到 event 是整个库的核心。 event 就是 Reactor 框架中的事件处理程序组件;它提供了函数接口,供 Reactor 在事件发生时调用,以执行相应的事件处理,通常它会绑定一个有效的句柄。 首先给出 event 结构体的声明,它位于libevent-master\include\event2\event_struct.h文件中:
1 struct event {
2 /**
3 * ev_callback,event的回调函数,被ev_base调用,执行事件处理程序,这是一个函数指针,原型为:
4 * void (*ev_callback)(int fd, short events, void *arg)
5 * 其中参数fd对应于ev_fd;events对应于ev_events;
6 *
7 * 具体定义在上面
8 * 以下为一些常用的宏定义
9 * #define ev_pri ev_evcallback.evcb_pri
10 * #define ev_flags ev_evcallback.evcb_flags
11 * #define ev_closure ev_evcallback.evcb_closure
12 * #define ev_callback ev_evcallback.evcb_cb_union.evcb_callback
13 * #define ev_arg ev_evcallback.evcb_arg
14 */
15 struct event_callback ev_evcallback; //event的回调函数,被ev_base调用
16
17 /* for managing timeouts */
18 /**
19 * min_heap_idx和ev_timeout,如果是timeout事件,它们是event在小根堆中的索引和超时值,
20 * libevent使用小根堆来管理定时事件
21 * 用来管理超时事件
22 */
23 union {
24 // 公用超时队列
25 TAILQ_ENTRY(event) ev_next_with_common_timeout;
26 // min_heap最小堆索引
27 int min_heap_idx;
28 } ev_timeout_pos;
29 evutil_socket_t ev_fd; //对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号;
30
31 short ev_events; //event关注的事件类型,它可以是以下3种类型:I/O事件、定时事件、信号、辅助选项(EV_PERSIST)
32 short ev_res; /* result passed to event callback 记录了当前激活事件的类型*/
33
34 struct event_base *ev_base; //该事件所属的反应堆实例,libevent句柄,每个事件都会保存一份句柄
35
36 /**
37 * 用共用体来同时表现IO事件和信号
38 * 以下为一些方便调用的宏定义
39 * mutually exclusive
40 * #define ev_signal_next ev_.ev_signal.ev_signal_next
41 * #define ev_io_next ev_.ev_io.ev_io_next
42 * #define ev_io_timeout ev_.ev_io.ev_timeout
43 *
44 * used only by signals
45 * #define ev_ncalls ev_.ev_signal.ev_ncalls
46 * #define ev_pncalls ev_.ev_signal.ev_pncalls
47 */
48
49
50
51 union {
52 /* used for io events */
53 struct {
54 // 下一个io事件
55 LIST_ENTRY (event) ev_io_next; //使用双向链表保存所有注册的I/O和Signal事件
56 // 事件超时时间(既可以是相对时间,也可以是绝对时间)
57 struct timeval ev_timeout;
58 } ev_io;
59
60 /* used by signal events */
61 struct {
62 // 下一个信号
63 LIST_ENTRY (event) ev_signal_next;
64 short ev_ncalls; //事件就绪执行时,调用ev_callback的次数,通常为1;
65 /* Allows deletes in callback */
66 short *ev_pncalls; //指针,通常指向ev_ncalls或者为NULL
67 } ev_signal;
68 } ev_;
69
70 // 保存事件的超时时间
71 struct timeval ev_timeout;
72 };
下面详细解释一下结构体中各字段的含义,注意部分变量有可能位于event_callback结构体中。
1)ev_events:event关注的事件类型,它可以是以下3种类型:
- I/O事件:EV_WRITE和EV_READ
- 定时事件:EV_TIMEOUT
- 信号事件: EV_SIGNAL
- 辅助选项:EV_PERSIST,表明是一个永久事件
Libevent中的定义为:
1 /** Indicates that a timeout has occurred. It's not necessary to pass
2 * this flag to event_for new()/event_assign() to get a timeout. */
3 // 定时事件
4 #define EV_TIMEOUT 0x01
5 /** Wait for a socket or FD to become readable */
6 // 读事件
7 #define EV_READ 0x02
8 /** Wait for a socket or FD to become writeable */
9 // 写事件
10 #define EV_WRITE 0x04
11 /** Wait for a POSIX signal to be raised*/
12 // 信号事件
13 #define EV_SIGNAL 0x08
14 /**
15 * Persistent event: won't get removed automatically when activated.
16 *
17 * When a persistent event with a timeout becomes activated, its timeout
18 * is reset to 0.
19 */
20 // 永久事件,激活执行后会重新加到队列中等待下一次激活,否则激活执行后会自动移除
21 #define EV_PERSIST 0x10
22 /** Select edge-triggered behavior, if supported by the backend. */
23 // 边沿触发,一般需要后台方法支持
24 #define EV_ET 0x20
25 /**
26 * If this option is provided, then event_del() will not block in one thread
27 * while waiting for the event callback to complete in another thread.
28 *
29 * To use this option safely, you may need to use event_finalize() or
30 * event_free_finalize() in order to safely tear down an event in a
31 * multithreaded application. See those functions for more information.
32 *
33 * THIS IS AN EXPERIMENTAL API. IT MIGHT CHANGE BEFORE THE LIBEVENT 2.1 SERIES
34 * BECOMES STABLE.
35 **/
36 // 终止事件,如果设置这个选项,则event_del不会阻塞,需要使用event_finalize或者
37 #define EV_FINALIZE 0x40
38 /**
39 * Detects connection close events. You can use this to detect when a
40 * connection has been closed, without having to read all the pending data
41 * from a connection.
42 *
43 * Not all backends support EV_CLOSED. To detect or require it, use the
44 * feature flag EV_FEATURE_EARLY_CLOSE.
45 **/
46 // 检查事件连接是否关闭;可以使用这个选项来检测链接是否关闭,而不需要读取此链接所有未决数据;
47 #define EV_CLOSED 0x80
可以看出事件类型可以使用“|”运算符进行组合,需要说明的是,信号和I/O事件不能同时设置; 还可以看出libevent使用event结构体将这3种事件的处理统一起来;
2)ev_next,ev_active_next 和 ev_signal_next 都是双向链表节点指针;它们是 libevent 对不同事件类型和在不同的时期,对事件的管理时使用到的字段。 libevent 使用双向链表保存所有注册的 I/O 和 Signal 事件,ev_next 就是该 I/O 事件在链表中的位置;称此链表为“已注册事件链表”; 同样 ev_signal_next 就是 signal 事件在 signal 事件链表中的位置; ev_active_next:libevent 将所有的激活事件放入到链表 active list 中,然后遍历 active list 执行调度,ev_active_next 就指明了 event 在 active list 中的位置;
3)min_heap_idx 和 ev_timeout,如果是 timeout 事件,它们是 event 在小根堆中的索引和超时值,libevent 使用小根堆来管理定时事件。
4)ev_base 该事件所属的反应堆实例,这是一个 event_base 结构体。
5)ev_fd,对于 I/O 事件,是绑定的文件描述符;对于 signal 事件,是绑定的信号;
6)ev_callback,是一个结构体,里面包含有事件的回调函数(54-57行),这个回调函数被 ev_base 调用,执行事件处理程序,原型为:
1 struct event_callback {
2 //下一个回调事件
3 TAILQ_ENTRY(event_callback) evcb_active_next;
4 /**
5 *
6 * 回调事件的状态标识,具体为:
7 * #define EVLIST_TIMEOUT 0x01 // event在time堆中,min_heap
8 * #define EVLIST_INSERTED 0x02 // event在已注册事件链表中,event_base的queue中
9 * #define EVLIST_SIGNAL 0x04 // 未见使用
10 * #define EVLIST_ACTIVE 0x08 // event在激活链表中,event_base的active_queue中
11 * #define EVLIST_INTERNAL 0x10 // 内部使用标记
12 * #define EVLIST_ACTIVE_LATER 0x20 event在下一次激活链表中
13 * #define EVLIST_INIT 0x80 // event已被初始化
14 * #define EVLIST_ALL 0xff // 主要用于判断事件状态的合法性
15 */
16 short evcb_flags;
17 // 回调函数的优先级,越小优先级越高
18 ev_uint8_t evcb_pri; /* smaller numbers are higher priority */
19 // 执行不同的回调函数
20 // /** @name Event closure codes
21
22 // Possible values for evcb_closure in struct event_callback
23
24 // @{
25 // */
26 // /** A regular event. Uses the evcb_callback callback 事件关闭时的回调函数模式类型 */
27 // // 常规事件,使用evcb_callback回调
28 // #define EV_CLOSURE_EVENT 0
29 // /** A signal event. Uses the evcb_callback callback */
30 // // 信号事件;使用evcb_callback回调
31 // #define EV_CLOSURE_EVENT_SIGNAL 1
32 // /** A persistent non-signal event. Uses the evcb_callback callback */
33 // // 永久性非信号事件;使用evcb_callback回调
34 // #define EV_CLOSURE_EVENT_PERSIST 2
35 // /** A simple callback. Uses the evcb_selfcb callback. */
36 // // 简单回调,使用evcb_selfcb回调
37 // #define EV_CLOSURE_CB_SELF 3
38 // /** A finalizing callback. Uses the evcb_cbfinalize callback. */
39 // // 结束的回调,使用evcb_cbfinalize回调
40 // #define EV_CLOSURE_CB_FINALIZE 4
41 // /** A finalizing event. Uses the evcb_evfinalize callback. */
42 // // 结束事件回调,使用evcb_evfinalize回调
43 // #define EV_CLOSURE_EVENT_FINALIZE 5
44 // /** A finalizing event that should get freed after. Uses the evcb_evfinalize
45 // * callback. */
46 // // 结束事件之后应该释放,使用evcb_evfinalize回调
47 // #define EV_CLOSURE_EVENT_FINALIZE_FREE 6
48 // /** @} */
49
50 ev_uint8_t evcb_closure;
51 /* allows us to adopt for different types of events */
52 // 允许我们自动适配不同类型的回调事件
53 union {
54 void (*evcb_callback)(evutil_socket_t, short, void *);
55 void (*evcb_selfcb)(struct event_callback *, void *);
56 void (*evcb_evfinalize)(struct event *, void *);
57 void (*evcb_cbfinalize)(struct event_callback *, void *);
58 } evcb_cb_union;
59 // 回调参数
60 void *evcb_arg;
61 };
7)ev_arg:void*,表明可以是任意类型的数据,在设置 event 时指定;
8)eb_flags:libevent 用于标记 event 信息的字段,表明其当前的状态,可能的值有:
1 //事件状态标志
2
3 // 事件在time min_heap堆中
4 #define EVLIST_TIMEOUT 0x01
5 // 事件在已注册事件链表中
6 #define EVLIST_INSERTED 0x02
7 // 目前未使用
8 #define EVLIST_SIGNAL 0x04
9 // 事件在激活链表中
10 #define EVLIST_ACTIVE 0x08
11 // 内部使用标记
12 #define EVLIST_INTERNAL 0x10
13 // 事件在下一次激活链表中
14 #define EVLIST_ACTIVE_LATER 0x20
15 // 事件已经终止
16 #define EVLIST_FINALIZING 0x40
17 // 事件初始化完成,但是哪儿都不在
18 #define EVLIST_INIT 0x80
19 // 包含所有事件状态,用于判断合法性的
20 #define EVLIST_ALL 0xff
9)ev_ncalls:事件就绪执行时,调用 ev_callback 的次数,通常为 1;
10)ev_pncalls:指针,通常指向 ev_ncalls 或者为 NULL;
11)ev_res:记录了当前激活事件的类型
5.2 libevent 对 event 的管理
从event 结构体中的 3 个链表节点指针和一个堆索引出发,大体上也能窥出 libevent 对 event 的管理方法了,可以参见下面的示意图。 每次当有事件 event 转变为就绪状态时,libevent 就会把它移入到 active event list[priority] 中,其中 priority 是 event 的优先级; 接着 libevent 会根据自己的调度策略选择就绪事件,调用其 cb_callback()函数执行事件处理;并根据就绪的句柄和事件类型填充 cb_callback 函数的参数。
5.3 事件设置的接口函数
要向 libevent 添加一个事件,需要首先设置 event 对象,这通过调用 libevent 提供的函数有:event_set(), event_base_set(), event_priority_set()来完成;下面分别进行讲解。
void event_set(struct event *ev, int fd, short events, void (*callback)(int, short, void *), void *arg)
1.设置事件 ev 绑定的文件描述符或者信号,对于定时事件,设为-1 即可;
2.设置事件类型,比如 EV_READ|EV_PERSIST, EV_WRITE, EV_SIGNAL 等;
3.设置事件的回调函数以及参数 arg;
4.初始化其它字段,比如缺省的 event_base 和优先级;
int event_base_set(struct event_base *base, struct event *ev)
设置 event ev 将要注册到的 event_base;
libevent 有一个全局 event_base 指针 current_base,默认情况下事件 ev 将被注册到 current_base 上,使用该函数可以指定不同的 event_base;
如果一个进程中存在多个 libevent 实例,则必须要调用该函数为 event 设置不同的 event_base;
int event_priority_set(struct event *ev, int pri)
设置event ev的优先级,没什么可说的,注意的一点就是:当ev正处于就绪状态时,不能设置,返回-1。
六、初见事件处理框架
前面已经对 libevent 的事件处理框架和 event 结构体做了描述,现在是时候剖析 libevent 对事件的详细处理流程了,本节将分析 libevent 的事件处理框架 event_base 和 libevent 注册、 删除事件的具体流程,可结合前一节 libevent 对 event 的管理。
6.1 事件处理框架-event_base
回想 Reactor 模式的几个基本组件,本节讲解的部分对应于 Reactor 框架组件。在 libevent 中,这就表现为 event_base 结构体,结构体声明如下,它位于libevent-master\event-internal.h 文件中:
1 struct event_base {
2 /** Function pointers and other data to describe this event_base's
3 * backend. */
4 /**
5 * 实际使用后台方法的句柄,实际上指向的是静态全局数组变量,从静态全局变量eventops中选择
6 */
7 const struct eventop *evsel;
8 /** Pointer to backend-specific data. */
9 /**
10 * 指向后台特定的数据,是由evsel->init返回的句柄
11 * 实际上是对实际后台方法所需数据的封装,void出于兼容性考虑
12 */
13 void *evbase;
14
15 /** List of changes to tell backend about at next dispatch. Only used
16 * by the O(1) backends. */
17 // 告诉后台方法下一次调度的变化列表
18 struct event_changelist changelist;
19
20 /** Function pointers used to describe the backend that this event_base
21 * uses for signals */
22 // 用于描述当前event_base用于信号的后台方法
23 const struct eventop *evsigsel;
24 /** Data to implement the common signal handler code. */
25 // 用于实现公用信号句柄的代码
26 struct evsig_info sig;
27
28 /** Number of virtual events */
29 // 虚拟事件的数量
30 int virtual_event_count;
31 /** Maximum number of virtual events active */
32 // 虚拟事件的最大数量
33 int virtual_event_count_max;
34 /** Number of total events added to this event_base */
35 // 添加到event_base上事件总数
36 int event_count;
37 /** Maximum number of total events added to this event_base */
38 // 添加到event_base上的最大个数
39 int event_count_max;
40 /** Number of total events active in this event_base */
41 // 当前event_base中活跃事件的个数
42 int event_count_active;
43 /** Maximum number of total events active in this event_base */
44 // 当前event_base中活跃事件的最大个数
45 int event_count_active_max;
46
47 /** Set if we should terminate the loop once we're done processing
48 * events. */
49 // 一旦我们完成处理事件了,如果我们应该终止loop,可以设置这个
50 int event_gotterm;
51 /** Set if we should terminate the loop immediately */
52 // 如果需要中止loop,可以设置这个变量
53 int event_break;
54 /** Set if we should start a new instance of the loop immediately. */
55 // 如果启动新实例的loop,可以设置这个
56 int event_continue;
57
58 /** The currently running priority of events */
59 // 当前运行事件的优先级
60 int event_running_priority;
61
62 /** Set if we're running the event_base_loop function, to prevent
63 * reentrant invocation. */
64 // 防止event_base_loop重入的
65 int running_loop;
66
67 /** Set to the number of deferred_cbs we've made 'active' in the
68 * loop. This is a hack to prevent starvation; it would be smarter
69 * to just use event_config_set_max_dispatch_interval's max_callbacks
70 * feature */
71 /**
72 * 设置已经在loop中设置为’active’的deferred_cbs的个数,这是为了避免
73 * 饥饿的hack方法;只需要使用event_config_set_max_dispatch_interval’s的
74 * max_callbacks特征就可以变的更智能
75 */
76 int n_deferreds_queued;
77
78 /* Active event management. // 活跃事件管理*/
79 /** An array of nactivequeues queues for active event_callbacks (ones
80 * that have triggered, and whose callbacks need to be called). Low
81 * priority numbers are more important, and stall higher ones.
82 * 存储激活事件的event_callbacks的队列,这些event_callbacks都需要调用;
83 * 数字越小优先级越高
84 */
85 struct evcallback_list *activequeues;
86 /** The length of the activequeues array 活跃队列的长度*/
87 int nactivequeues;
88 /** A list of event_callbacks that should become active the next time
89 * we process events, but not this time. */
90 // 下一次会变成激活状态的回调函数的列表,但是当前这次不会调用
91 struct evcallback_list active_later_queue;
92
93 /* common timeout logic // 公用超时逻辑*/
94
95 /** An array of common_timeout_list* for all of the common timeout
96 * values we know.
97 * 公用超时事件列表,这是二级指针,每个元素都是具有同样超时
98 * 时间事件的列表,
99 */
100 struct common_timeout_list **common_timeout_queues;
101 /** The number of entries used in common_timeout_queues */
102 // 公用超时队列中的项目个数
103 int n_common_timeouts;
104 /** The total size of common_timeout_queues. */
105 // 公用超时队列的总个数
106 int n_common_timeouts_allocated;
107
108 /** Mapping from file descriptors to enabled (added) events */
109 // 文件描述符和事件之间的映射表
110 struct event_io_map io;
111
112 /** Mapping from signal numbers to enabled (added) events. */
113 // 信号数字和事件之间映射表
114 struct event_signal_map sigmap;
115
116 /** Priority queue of events with timeouts. */
117 // 事件超时的优先级队列,使用最小堆实现
118 struct min_heap timeheap;
119
120 /** Stored timeval: used to avoid calling gettimeofday/clock_gettime
121 * too often. */
122 // 存储时间:用来避免频繁调用gettimeofday/clock_gettime
123 struct timeval tv_cache;
124
125 // monotonic格式的时间
126 struct evutil_monotonic_timer monotonic_timer;
127
128 /** Difference between internal time (maybe from clock_gettime) and
129 * gettimeofday. */
130 // 内部时间(可以从clock_gettime获取)和gettimeofday之间的差异
131 struct timeval tv_clock_diff;
132 /** Second in which we last updated tv_clock_diff, in monotonic time. */
133 // 更新内部时间的间隔秒数
134 time_t last_updated_clock_diff;
135
136 #ifndef EVENT__DISABLE_THREAD_SUPPORT
137 /* threading support */
138 /** The thread currently running the event_loop for this base */
139 unsigned long th_owner_id;
140 /** A lock to prevent conflicting accesses to this event_base */
141 void *th_base_lock;
142 /** A condition that gets signalled when we're done processing an
143 * event with waiters on it. */
144 void *current_event_cond;
145 /** Number of threads blocking on current_event_cond. */
146 int current_event_waiters;
147 #endif
148 /** The event whose callback is executing right now */
149 // 当前执行的回调函数
150 struct event_callback *current_event;
151
152 #ifdef _WIN32
153 /** IOCP support structure, if IOCP is enabled. */
154 struct event_iocp_port *iocp;
155 #endif
156
157 /** Flags that this base was configured with */
158
159 // event_base配置的特征值
160 // 多线程调用是不安全的,单线程非阻塞模式
161 // EVENT_BASE_FLAG_NOLOCK = 0x01,
162 // 忽略检查EVENT_*等环境变量
163 // EVENT_BASE_FLAG_IGNORE_ENV = 0x02,
164 // 只用于windows
165 // EVENT_BASE_FLAG_STARTUP_IOCP = 0x04,
166 // 不使用缓存的时间,每次回调都会获取系统时间
167 // EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
168 // 如果使用epoll方法,则使用epoll内部的changelist
169 // EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10,
170 // 使用更精确的时间,但是可能性能会降低
171 // EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
172
173 enum event_base_config_flag flags;
174
175 // 最大调度时间间隔
176 struct timeval max_dispatch_time;
177 // 最大调度的回调函数个数
178 int max_dispatch_callbacks;
179 // 优先级设置之后,对于活跃队列中子队列个数的限制
180 // 但是当子队列个数超过这个限制之后,会以实际的回调函数个数为准
181 int limit_callbacks_after_prio;
182
183 /* Notify main thread to wake up break, etc. */
184 /** True if the base already has a pending notify, and we don't need
185 * to add any more. */
186 //如果为1表示当前可以唤醒主线程,否则不能唤醒主线程
187 int is_notify_pending;
188 /** A socketpair used by some th_notify functions to wake up the main
189 * thread. */
190 // 一端读、一端写,用来触发唤醒事件
191 evutil_socket_t th_notify_fd[2];
192 /** An event used by some th_notify functions to wake up the main
193 * thread. */
194 // 唤醒event_base的event,被添加到监听集合中的对象
195 struct event th_notify;
196 /** A function used to wake up the main thread from another thread. */
197 //执行唤醒操作的函数(不是唤醒event的回调函数)
198 int (*th_notify_fn)(struct event_base *base);
199
200 /** Saved seed for weak random number generator. Some backends use
201 * this to produce fairness among sockets. Protected by th_base_lock. */
202 // 保存弱随机数产生器的种子。某些后台方法会使用这个种子来公平的选择sockets。
203 struct evutil_weakrand_state weakrand_seed;
204
205 /** List of event_onces that have not yet fired. */
206 LIST_HEAD(once_event_list, event_once) once_events;
207
208 };
下面详细解释一下结构体中部分字段的含义。
1)evsel 和 evbase 这两个字段的设置可能会让人有些迷惑,这里你可以把 evsel 和 evbase 看作是类和静态函数的关系,比如添加事件时的调用行为:evsel->add(evbase, ev),实际执 行操作的是 evbase;这相当于 class::add(instance, ev),instance 就是 class 的一个对象实例。 evsel指向了全局变量static const struct eventop *eventops[]中的一个;libevent将系统提供的I/O demultiplex机制统一封装成了eventop结构;因此 eventops[]包含了select、poll、kequeue和epoll等等其中的若干个全局实例对象。 evbase实际上是一个eventop实例对象; 先来看看eventop结构体,它的成员是一系列的函数指针, 在event-internal.h文件中:
1 /** Structure to define the backend of a given event_base. */
2 struct eventop {
3 /** The name of this backend. 后台方法名字,即epoll,select,poll等*/
4 const char *name;
5 /** Function to set up an event_base to use this backend. It should
6 * create a new structure holding whatever information is needed to
7 * run the backend, and return it. The returned pointer will get
8 * stored by event_init into the event_base.evbase field. On failure,
9 * this function should return NULL. */
10 /**
11 * 配置libevent句柄event_base使用当前后台方法;他应该创建新的数据结构,
12 * 隐藏了后台方法运行所需的信息,然后返回这些信息的结构体,为了支持多种
13 * 结构体,因此返回void*;返回的指针将保存在event_base.evbase中;如果失败,
14 * 将返回NULL
15 */
16 void *(*init)(struct event_base *);
17 /** Enable reading/writing on a given fd or signal. 'events' will be
18 * the events that we're trying to enable: one or more of EV_READ,
19 * EV_WRITE, EV_SIGNAL, and EV_ET. 'old' will be those events that
20 * were enabled on this fd previously. 'fdinfo' will be a structure
21 * associated with the fd by the evmap; its size is defined by the
22 * fdinfo field below. It will be set to 0 the first time the fd is
23 * added. The function should return 0 on success and -1 on error.
24 */
25 /**
26 * 使给定的文件描述符或者信号变得可读或者可写。’events’将是我们尝试添加的
27 * 事件类型:一个或者更多的EV_READ,EV_WRITE,EV_SIGNAL,EV_ET。’old’是这些事件
28 * 先前的事件类型;’fdinfo’将是fd在evmap中的辅助结构体信息,它的大小由下面的
29 * fdinfo_len给出。fd第一次添加时将设置为0.成功则返回0,失败则返回-1
30 */
31 int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
32 /** As "add", except 'events' contains the events we mean to disable. */
33 // 删除事件
34 int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
35 /** Function to implement the core of an event loop. It must see which
36 added events are ready, and cause event_active to be called for each
37 active event (usually via event_io_active or such). It should
38 return 0 on success and -1 on error.
39 */
40 /**
41 * event_loop实现的核心代码。他必须察觉哪些添加的事件已经准备好,然后触发每个
42 * 活跃事件都被调用(通常是通过event_io_active或者类似这样)。成功返回0,失败则-1
43 */
44 int (*dispatch)(struct event_base *, struct timeval *);
45 /** Function to clean up and free our data from the event_base. */
46 // 清除event_base并释放数据
47 void (*dealloc)(struct event_base *);
48 /** Flag: set if we need to reinitialize the event base after we fork.
49 */
50 // 在执行fork之后是否需要重新初始化的标识位
51 int need_reinit;
52 /** Bit-array of supported event_method_features that this backend can
53 * provide. */
54
55 // 后台方法可以提供的特征
56 // enum event_method_feature {
57 // 边沿触发
58 // EV_FEATURE_ET = 0x01,
59 // 要求事后台方法在调度很多事件时大约为O(1)操作,select和poll无法提供这种特征,
60 // 这两种方法具有N个事件时,可以提供O(N)操作
61 // EV_FEATURE_O1 = 0x02,
62
63 // 后台方法可以处理各种文件描述符,而不仅仅是sockets
64 // EV_FEATURE_FDS = 0x04,
65 /** Require an event method that allows you to use EV_CLOSED to detect
66 * connection close without the necessity of reading all the pending data.
67 *
68 * Methods that do support EV_CLOSED may not be able to provide support on
69 * all kernel versions.
70 **/
71 // 要求后台方法允许使用EV_CLOSED特征检测链接是否中断,而不需要读取
72 // 所有未决数据;但是不是所有内核都能提供这种特征
73 // EV_FEATURE_EARLY_CLOSE = 0x08
74 // };
75
76 enum event_method_feature features;
77 /** Length of the extra information we should record for each fd that
78 has one or more active events. This information is recorded
79 as part of the evmap entry for each fd, and passed as an argument
80 to the add and del functions above.
81 */
82 /**
83 * 应该为每个文件描述符保留的额外信息长度,额外信息可能包括一个或者多个
84 * 活跃事件。这个信息是存储在每个文件描述符的evmap中,然后通过参数传递
85 * 到上面的add和del函数中。
86 */
87 size_t fdinfo_len;
88 };
也就是说,在 libevent 中,每种 I/O demultiplex 机制的实现都必须提供这五个函数接口, 来完成自身的初始化、销毁释放;对事件的注册、注销和分发。 比如对于 epoll,libevent 实现了 5 个对应的接口函数,并在初始化时并将 eventop 的 5 个函数指针指向这 5 个函数,那么程序就可以使用 epoll 作为 I/O demultiplex 机制了,这个在后面会再次提到。
2)activequeues 是一个一级指针,前面讲过 libevent 支持事件优先级,因此你可以把它看作是一维数组,其中的元素 activequeues[priority]是一个链表,链表的每个节点指向一个优先级为 priority 的就绪事件 event,实际上链表当中存放的是事件的回调函数。
3)io,管理IO事件的结构体变量。
4)sigmap 是由来管理信号的结构体,将在后面信号处理时专门讲解;
5)timeheap 是管理定时事件的小根堆,将在后面定时事件处理时专门讲解;
6)tv_cache 是 libevent 用于时间管理的变量,存储时间:用来避免频繁调用gettimeofday/clock_gettime;
6.2 创建和初始化 event_base
创建一个 event_base 对象也既是创建了一个新的 libevent 实例,程序需要通过调用 event_init()(内部调用 event_base_new 函数执行具体操作)函数来创建,该函数同时还对新生成的 libevent 实例进行了初始化。 该函数首先为 event_base 实例申请空间,然后初始化定时事件使用的mini-heap,选择并初始化合适的系统 I/O 的 demultiplexer 机制,初始化各事件链表; 函数还检测了系统的时间设置,为后面的时间管理打下基础。
6.3 接口函数
前面提到 Reactor 框架的作用就是提供事件的注册、注销接口;根据系统提供的事件多路分发机制执行事件循环,当有事件进入“就绪”状态时,调用注册事件的回调函数来处理事件。 Libevent 中对应的接口函数主要就是:
int event_add(struct event *ev, const struct timeval *timeout);
int event_del(struct event *ev);
int event_base_loop(struct event_base *base, int loops);
void event_active(struct event *event, int res, short events);
void event_process_active(struct event_base *base);
6.3.1 注册事件
函数原型:
1 /**
2 * 事件注册-event_add
3 * 1、将事件添加到等待事件中去,需要注意的是,event_add在event_new或者event_assign之后执行,
4 * 即添加的事件必须是经过基本初始化过后的事件;
5 * 2、此处添加的事件包括IO事件、信号事件、定时事件,根据事件申请时设置的事件类型决定添加的流程;
6 * 3、超时控制包括两种方式:
7 * (1)最小堆:时间超时时间存储在最小堆,每次执行超时任务都从最小堆堆顶取任务执行
8 * (2)最小堆+公用超时队列:相同超时的任务存储在同一个超时队列,每一个超时队列的队首事件存储在最小堆,
9 * 每次执行超时任务时都从最小堆堆顶取任务执行,然后遍历执行该任务所在公用超时队列中的所有超时任务。
10 */
11 int event_add(struct event *ev, const struct timeval *tv)
参数:ev:指向要注册的事件;
tv:超时时间;
函数将 ev 注册到 ev->ev_base 上,事件类型由 ev->ev_events 指明,如果注册成功,ev 将被插入到已注册链表中;如果 tv 不是 NULL,则会同时注册定时事件,将 ev 添加到 timer 堆上; 如果其中有一步操作失败,那么函数保证没有事件会被注册,可以讲这相当于一个原子操作。
1 int event_add(struct event *ev, const struct timeval *tv)
2 {
3 int res;
4
5 if (EVUTIL_FAILURE_CHECK(!ev->ev_base)) {
6 event_warnx("%s: event has no event_base set.", __func__);
7 return -1;
8 }
9
10 EVBASE_ACQUIRE_LOCK(ev->ev_base, th_base_lock);//加锁
11
12 // 实际时调用内部实现函数event_add_nolock实现的,下文会分析
13 res = event_add_nolock_(ev, tv, 0);
14
15 EVBASE_RELEASE_LOCK(ev->ev_base, th_base_lock);
16
17 return (res);
18 }
event_add_nolock()函数
1 /* Implementation function to add an event. Works just like event_add,
2 * except: 1) it requires that we have the lock. 2) if tv_is_absolute is set,
3 * we treat tv as an absolute time, not as an interval to add to the current
4 * time
5 * 此函数真正实现将事件添加到event_base的等待列表中。
6 * 真正的将信号事件注册在event_base上,也就是进行了信号的内部事件注册
7 *
8 * 添加事件的实现函数;就像event_add一样,异常:
9 * 1)它需要使用者加锁;2)如果tv_is_absolute设置了,则将tv作为绝对时间对待,而不是相对于当前添加时间的时间间隔
10 */
11 int
12 event_add_nolock_(struct event *ev, const struct timeval *tv,
13 int tv_is_absolute)
14 {
15 struct event_base *base = ev->ev_base;
16 int res = 0;
17 int notify = 0;
18
19 EVENT_BASE_ASSERT_LOCKED(base);
20 event_debug_assert_is_setup_(ev);
21
22 event_debug((
23 "event_add: event: %p (fd "EV_SOCK_FMT"), %s%s%s%scall %p",
24 ev,
25 EV_SOCK_ARG(ev->ev_fd),
26 ev->ev_events & EV_READ ? "EV_READ " : " ",
27 ev->ev_events & EV_WRITE ? "EV_WRITE " : " ",
28 ev->ev_events & EV_CLOSED ? "EV_CLOSED " : " ",
29 tv ? "EV_TIMEOUT " : " ",
30 ev->ev_callback));
31
32 // 事件状态必须处于合法的某种事件状态,否则报错
33 EVUTIL_ASSERT(!(ev->ev_flags & ~EVLIST_ALL));
34
35 // 已经处于结束状态的事件再次添加会报错
36 if (ev->ev_flags & EVLIST_FINALIZING) {
37 /* XXXX debug */
38 return (-1);
39 }
40
41 /*
42 * prepare for timeout insertion further below, if we get a
43 * failure on any step, we should not change any state.
44 */
45 /**
46 * 为超时插入做准备,如果超时控制不为空,且事件没有处于超时状态
47 * 首先为将要插入的超时事件准备插入节点,主要是为了防止后面出现这种情况:
48 * 事件状态改变已经完成,但是最小堆申请节点却失败;
49 * 因此,如果在任何一步出现错误,都不能改变事件状态,这是前提条件。
50 */
51
52 if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {
53 if (min_heap_reserve_(&base->timeheap,
54 1 + min_heap_size_(&base->timeheap)) == -1)
55 return (-1); /* ENOMEM == errno */
56 }
57
58 /* If the main thread is currently executing a signal event's
59 * callback, and we are not the main thread, then we want to wait
60 * until the callback is done before we mess with the event, or else
61 * we can race on ev_ncalls and ev_pncalls below. */
62 /**
63 * 如果主线程当前正在执行信号事件的回调函数,同时又不在主线程,则
64 * 需要等待回调函数执行完毕才能继续添加事件,否则可能会在
65 * ev_ncalls和ev_pncalls上产生竞争。
66 */
67 #ifndef EVENT__DISABLE_THREAD_SUPPORT
68 if (base->current_event == event_to_event_callback(ev) &&
69 (ev->ev_events & EV_SIGNAL)
70 && !EVBASE_IN_THREAD(base)) {
71 ++base->current_event_waiters;
72 EVTHREAD_COND_WAIT(base->current_event_cond, base->th_base_lock);
73 }
74 #endif
75
76 /**
77 * 如果事件类型是IO事件/信号事件,同时事件状态不是已经插入/激活/下一次激活状态,
78 * 则根据事件类型将事件添加到不同的映射表或者队列中
79 */
80 if ((ev->ev_events & (EV_READ|EV_WRITE|EV_CLOSED|EV_SIGNAL)) &&
81 !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE|EVLIST_ACTIVE_LATER))) {
82 // 如果事件是IO事件,则将事件插入到IO事件与文件描述符的映射表中
83 if (ev->ev_events & (EV_READ|EV_WRITE|EV_CLOSED))
84 res = evmap_io_add_(base, ev->ev_fd, ev);
85 // 如果事件是信号事件,则将事件插入信号与文件描述符的映射表中
86 else if (ev->ev_events & EV_SIGNAL)
87 res = evmap_signal_add_(base, (int)ev->ev_fd, ev);
88 // 如果上述添加行为正确,则将事件插入到event_base的事件列表中
89 if (res != -1)
90 event_queue_insert_inserted(base, ev);//其实就是这是事件的已注册标志
91 /**
92 * 如果上述添加行为正确,则设置通知主线程的标志,因为已经添加了新事件,
93 * 防止1)优先级高的事件被优先级低的事件倒挂,2)防止主线程忙等,会通知主线程有新事件
94 */
95 if (res == 1) {
96 /* evmap says we need to notify the main thread. */
97 notify = 1;
98 res = 0;
99 }
100 }
101
102 /*
103 * we should change the timeout state only if the previous event
104 * addition succeeded.
105 * 只有当前面事件条件成功执行之后,才能改变超时状态
106 */
107 if (res != -1 && tv != NULL) {
108 struct timeval now;
109 int common_timeout;
110 #ifdef USE_REINSERT_TIMEOUT
111 int was_common;
112 int old_timeout_idx;
113 #endif
114
115 /*
116 * for persistent timeout events, we remember the
117 * timeout value and re-add the event.
118 *
119 * If tv_is_absolute, this was already set.
120 *
121 * 对于持久化的定时事件,需要记住超时时间,并重新注册事件
122 * 如果tv_is_absolute设置,则事件超时时间就等于输入时间参数
123 */
124 if (ev->ev_closure == EV_CLOSURE_EVENT_PERSIST && !tv_is_absolute)
125 ev->ev_io_timeout = *tv;
126
127 /**
128 * 如果没有使用USE_REINSERT_TIMEOUT,则当事件处于超时状态时,需要从队列中移除事件
129 * 因为同样的事件不能重新插入,所以当一个事件已经处于超时状态时,为防止执行,需要先移除后插入
130 */
131 #ifndef USE_REINSERT_TIMEOUT
132 if (ev->ev_flags & EVLIST_TIMEOUT) {
133 event_queue_remove_timeout(base, ev);
134 }
135 #endif
136
137 /* Check if it is active due to a timeout. Rescheduling
138 * this timeout before the callback can be executed
139 * removes it from the active list.
140 * 检查事件当前状态是否已经激活,而且是超时事件的激活状态,
141 * 则在回调函数执行之前,需要重新调度这个超时事件,因此需要把它移出激活队列
142 * */
143 if ((ev->ev_flags & EVLIST_ACTIVE) &&
144 (ev->ev_res & EV_TIMEOUT)) {
145 if (ev->ev_events & EV_SIGNAL) {
146 /* See if we are just active executing
147 * this event in a loop
148 */
149 if (ev->ev_ncalls && ev->ev_pncalls) {
150 /* Abort loop */
151 *ev->ev_pncalls = 0;
152 }
153 }
154
155 // 将此事件的回调函数从激活队列中移除
156 event_queue_remove_active(base, event_to_event_callback(ev));
157 }
158
159 // 获取base中的缓存时间
160 gettime(base, &now);
161
162 // 检查base是否使用了公用超时队列机制
163 common_timeout = is_common_timeout(tv, base);
164 #ifdef USE_REINSERT_TIMEOUT
165 was_common = is_common_timeout(&ev->ev_timeout, base);
166 old_timeout_idx = COMMON_TIMEOUT_IDX(&ev->ev_timeout);
167 #endif
168
169 /**
170 * 1)如果设置绝对超时时间,则设置时间超时时间为输入时间参数
171 * 2)如果使用的公用超时队列机制,则根据当前base中时间和输入超时时间间隔计算出时间超时时间,
172 * 并对超时时间进行公用超时掩码计算
173 * 3)如果是其他情况,则直接根据base中时间和输入超时时间间隔计算事件的超时时间
174 */
175 if (tv_is_absolute) {
176 ev->ev_timeout = *tv;
177 } else if (common_timeout) {
178 struct timeval tmp = *tv;
179 tmp.tv_usec &= MICROSECONDS_MASK;
180 evutil_timeradd(&now, &tmp, &ev->ev_timeout);
181 ev->ev_timeout.tv_usec |=
182 (tv->tv_usec & ~MICROSECONDS_MASK);
183 } else {
184 evutil_timeradd(&now, tv, &ev->ev_timeout);
185 }
186
187 event_debug((
188 "event_add: event %p, timeout in %d seconds %d useconds, call %p",
189 ev, (int)tv->tv_sec, (int)tv->tv_usec, ev->ev_callback));
190
191 // 将事件插入超时队列
192 #ifdef USE_REINSERT_TIMEOUT
193 // event_queue_reinsert_timeout会插入两个队列:一个是公用超时队列,一个超时队列
194 event_queue_reinsert_timeout(base, ev, was_common, common_timeout, old_timeout_idx);
195 #else
196 // 只会插入超时队列
197 event_queue_insert_timeout(base, ev);
198 #endif
199
200 /**
201 * 如果使用了公用超时队列机制,则需要根据当前事件的超时时间将当前事件插入具有相同超时时间的时间列表
202 */
203 if (common_timeout) {
204 // 根据事件超时时间获取应该插入的公用超时队列,注意此处是从队尾插入
205 struct common_timeout_list *ctl =
206 get_common_timeout_list(base, &ev->ev_timeout);
207 /**
208 * 如果当前事件是公用超时队列的第一个事件,则因此需要将此超时事件插入最小堆
209 * 解释:公用超时队列机制:处于同一个公用超时队列中的所有事件具有相同的超时控制,因此只需要将公用超时队列
210 * 的第一个事件插入最小堆,当超时触发时,可以通过遍历公用超时队列获取同样的超时事件。
211 */
212 if (ev == TAILQ_FIRST(&ctl->events)) {
213 common_timeout_schedule(ctl, &now, ev);
214 }
215 } else {
216 // 如果没有使用公用超时队列,则调整最小堆
217 struct event* top = NULL;
218 /* See if the earliest timeout is now earlier than it
219 * was before: if so, we will need to tell the main
220 * thread to wake up earlier than it would otherwise.
221 * We double check the timeout of the top element to
222 * handle time distortions due to system suspension.
223 * 查看当前事件是否位于最小堆根部,如果是,则需要通知主线程
224 * 否则,需要查看最小堆根部超时时间是否已经小于当前时间,即已经超时了,如果是,则需要通知主线程
225 */
226 if (min_heap_elt_is_top_(ev))
227 notify = 1;
228 else if ((top = min_heap_top_(&base->timeheap)) != NULL &&
229 evutil_timercmp(&top->ev_timeout, &now, <))
230 notify = 1;
231 }
232 }
233
234 /**
235 * if we are not in the right thread, we need to wake up the loop
236 * 如果本线程不是执行event_loop的主线程,就通知主线程
237 * */
238 if (res != -1 && notify && EVBASE_NEED_NOTIFY(base))
239 evthread_notify_base(base);
240
241 event_debug_note_add_(ev);
242
243 return (res);
244 }
6.3.2 删除事件
函数原型为:
1 static int event_del_(struct event *ev, int blocking)
该函数将删除事件 ev,对于 I/O 事件,从 I/O 的 demultiplexer 上将事件注销;对于 Signal 事件,将从 Signal 事件链表中删除;对于定时事件,将从堆上删除; 同样删除事件的操作则不一定是原子的,比如删除时间事件之后,有可能从系统 I/O 机 制中注销会失败。
1 static int
2 event_del_(struct event *ev, int blocking)
3 {
4 int res;
5 struct event_base *base = ev->ev_base;
6
7 if (EVUTIL_FAILURE_CHECK(!base)) {
8 event_warnx("%s: event has no event_base set.", __func__);
9 return -1;
10 }
11
12 EVBASE_ACQUIRE_LOCK(base, th_base_lock);
13 res = event_del_nolock_(ev, blocking);
14 EVBASE_RELEASE_LOCK(base, th_base_lock);
15
16 return (res);
17 }
七、事件主循环
现在我们已经初步了解了 libevent 的 Reactor 组件——event_base 和事件管理框架,接下 来就是 libevent 事件处理的中心部分——事件主循环,根据系统提供的事件多路分发机制执 行事件循环,对已注册的就绪事件,调用注册事件的回调函数来处理事件。
Libevent 将 I/O 事件、定时器和信号事件处理很好的结合到了一起,本节也会介绍 libevent 是如何做到这一点的。 在看完本节的内容后,读者应该会对 Libevent 的基本框架:事件管理和主循环有比较清晰的认识了,并能够把 libevent 的事件控制流程清晰的串通起来,剩下的就是一些细节的内容了。
7.1 事件处理主循环
事件处理主循环 Libevent 的事件主循环主要是通过 event_base_loop ()函数完成的,其主要操作如下面的流程 图所示,event_base_loop 所作的就是持续执行下面的循环。
清楚了 event_base_loop 所作的主要操作,就可以对比源代码看个究竟了,代码结构还是相当清晰的,具体的分析看代码注释即可。
1 /**
2 * 等待事件变为活跃,然后运行事件回调函数。
3 * 相比event_base_dispatch函数,这个函数更为灵活。默认情况下,loop会一直运行到没有等待事件或者激活的事件,或者
4 * 运行到调用event_base_loopbreak或者event_base_loopexit函数。你可以使用’flags‘调整loop行为。
5 * 参数 eb:event_base_new或者event_base_new_with_config产生的event_base结构体
6 * flags:可以是EVLOOP_ONCE|EVLOOP_NONBLOCK
7 * 返回值:成功则为0,失败则为-1,如果因为没有等待的事件或者激活事件而退出则返回1
8 * 相关查看event_base_loopexit,event_base_dispatch
9 */
10
11
12 int
13 event_base_loop(struct event_base *base, int flags)
14 {
15 const struct eventop *evsel = base->evsel;
16 struct timeval tv;
17 struct timeval *tv_p;
18 int res, done, retval = 0;
19
20 /* Grab the lock. We will release it inside evsel.dispatch, and again
21 * as we invoke user callbacks. */
22 EVBASE_ACQUIRE_LOCK(base, th_base_lock);
23
24 if (base->running_loop) {
25 event_warnx("%s: reentrant invocation. Only one event_base_loop"
26 " can run on each event_base at once.", __func__);
27 EVBASE_RELEASE_LOCK(base, th_base_lock);
28 return -1;
29 }
30
31 base->running_loop = 1;
32
33 // 清空当前event_base中的时间,防止误用
34 clear_time_cache(base);
35
36 // 如果event_base中有信号事件,则需要设置
37 if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)
38 evsig_set_base_(base);
39
40 done = 0;
41
42 #ifndef EVENT__DISABLE_THREAD_SUPPORT
43 base->th_owner_id = EVTHREAD_GET_ID();
44 #endif
45
46 base->event_gotterm = base->event_break = 0;
47
48 while (!done) {
49 base->event_continue = 0;
50 base->n_deferreds_queued = 0;
51
52 /* Terminate the loop if we have been asked to */
53 // 每次loop时,需要判定是否别的地方已经设置了终止或者退出的标志位
54 if (base->event_gotterm) {
55 break;
56 }
57
58 if (base->event_break) {
59 break;
60 }
61
62 tv_p = &tv;
63
64 /**
65 * 如果event_base的活跃事件数量为空并且是非阻塞模式,则获取下一个超时事件的距离超时的时间间隔,用于后台
66 * 方法用于调度超时事件,否则清空存储距离超时的时间间隔。
67 * 下文分析
68 */
69 if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
70 timeout_next(base, &tv_p);
71 } else {
72 /*
73 * if we have active events, we just poll new events
74 * without waiting.
75 */
76 evutil_timerclear(&tv);
77 }
78
79 /* If we have no events, we just exit 没有设置EVLOOP_NO_EXIT_ON_EMPTY标志,当没有激活事件发生时,就退出*/
80 if (0==(flags&EVLOOP_NO_EXIT_ON_EMPTY) &&
81 !event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
82 event_debug(("%s: no events registered.", __func__));
83 retval = 1;
84 goto done;
85 }
86
87 // 将下一次激活事件队列中的事件都移动到激活队列中
88 event_queue_make_later_events_active(base);
89
90 // 清空event_base中时间,防止误用
91 clear_time_cache(base);
92
93 // 调用后台方法的dispatch方法,如果后台方法是epoll的话就调用epoll_dispatch
94 res = evsel->dispatch(base, tv_p);
95
96 if (res == -1) {
97 event_debug(("%s: dispatch returned unsuccessfully.",
98 __func__));
99 retval = -1;
100 goto done;
101 }
102
103 // 更新当前event_base中缓存的时间,因为这是在执行后台调度方法之后的时间,可以用来作为超时事件的参考时间
104 update_time_cache(base);
105
106 // 主要是从超时事件最小堆中取出超时事件,并将超时事件放入激活队列
107 timeout_process(base);
108
109 /**
110 * 如果激活队列不为空,则处理激活的事件
111 * 否则,如果模式为非阻塞,则退出loop
112 */
113 if (N_ACTIVE_CALLBACKS(base)) {
114 //通过event_process_active函数来处理已经激活的event
115 int n = event_process_active(base);
116 if ((flags & EVLOOP_ONCE)
117 && N_ACTIVE_CALLBACKS(base) == 0
118 && n != 0)//如果没有激活事件就等待,至少处理一次激活时间
119 done = 1;
120 } else if (flags & EVLOOP_NONBLOCK)//没有激活事件立即退出
121 done = 1;
122 }
123 event_debug(("%s: asked to terminate loop.", __func__));
124
125 done:
126 clear_time_cache(base);
127 base->running_loop = 0;
128
129 EVBASE_RELEASE_LOCK(base, th_base_lock);
130
131 return (retval);
132 }
7.2 I/O 和 Timer 事件的统一
Libevent 将 Timer 和 Signal 事件都统一到了系统的 I/O 的 demultiplex 机制中了。
首先将 Timer 事件融合到系统 I/O 多路复用机制中,还是相当清晰的,因为系统的 I/O 机制像 select()和 epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间) timeout,即使没有 I/O 事件发生,它们也保证能在 timeout 时间内返回。
那么根据所有 Timer 事件的最小超时时间来设置系统 I/O 的 timeout 时间;当系统 I/O 返回时,再激活所有就绪的 Timer 事件就可以了,这样就能将 Timer 事件完美的融合到系统 的 I/O 机制中了。
这是在 Reactor 和 Proactor 模式(主动器模式,比如 Windows 上的 IOCP)中处理 Timer 事件的经典方法了,ACE 采用的也是这种方法,大家可以参考 POSA vol2 书中的 Reactor 模式一节。
堆是一种经典的数据结构,向堆中插入、删除元素时间复杂度都是 O(lgN),N 为堆中元素的个数,而获取最小 key 值(小根堆)的复杂度为 O(1);因此变成了管理 Timer 事件的 绝佳人选(当然是非唯一的),libevent 就是采用的堆结构。
7.3 I/O 和 Signal 事件的统一
Signal 是异步事件的经典事例,将 Signal 事件统一到系统的 I/O 多路复用中就不像 Timer 事件那么自然了,Signal 事件的出现对于进程来讲是完全随机的,进程不能只是测试一个变量来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行如下的操作”。
如果当 Signal 发生时,并不立即调用 event 的 callback 函数处理信号,而是设法通知系统的 I/O 机制,让其返回,然后再统一和 I/O 事件以及 Timer 一起处理,不就可以了嘛。是的,这也是 libevent 中使用的方法。
问题的核心在于,当 Signal 发生时,如何通知系统的 I/O 多路复用机制,这里先买个小关子,放到信号处理一节再详细说明,首先想到的是使用 pipe。
八、集成信号处理
现在我们已经了解了 libevent 的基本框架:事件管理框架和事件主循环。上节提到了 libevent 中 I/O 事件和 Signal 以及 Timer 事件的集成,这一节将分析如何将 Signal 集成到事 件主循环的框架中。
8.1 集成策略——使用 socket pair
前一节已经做了足够多的介绍了,基本方法就是采用“消息机制”。在 libevent 中这是通过 socket pair 完成的,下面就来详细分析一下。 Socket pair 就是一个 socket 对,包含两个 socket,一个读 socket,一个写 socket。工作 方式如下图所示:
创建一个 socket pair 并不是复杂的操作,可以参见下面的流程图,清晰起见,其中忽略 了一些错误处理和检查。
Libevent 提供了辅助函数 evutil_socketpair()来创建一个 socket pair,可以结合上面的创建流程来分析该函数。
8.2 集成到事件主循环——通知 event_base
Socket pair 创建好了,可是 libevent 的事件主循环还是不知道 Signal 是否发生了啊,看来我们还差了最后一步,那就是:为 socket pair 的读 socket 在 libevent 的 event_base 实例 上注册一个 persist(持久的) 的读事件。 这样当向写 socket 写入数据时,读 socket 就会得到通知,触发读事件,从而 event_base 就能相应的得到通知了。
前面提到过,Libevent 会在事件主循环中检查标记,来确定是否有触发的 signal,如果 标记被设置就处理这些 signal,这段代码在各个具体的 I/O 机制中,以 Epoll 为例,在 epoll_dispatch()函数中,代码如下:
1 static int
2 epoll_dispatch(struct event_base *base, struct timeval *tv)
3 {
4 /**
5 * 后台数据结构,包括
6 * struct epoll_event *events;
7 * // epoll中当前注册的事件总数
8 * int nevents;
9 * // epoll返回的句柄
10 * int epfd;
11 * #ifdef USING_TIMERFD
12 * int timerfd;
13 * #endif
14 * };
15 */
16 struct epollop *epollop = base->evbase;
17 struct epoll_event *events = epollop->events;
18 int i, res;
19 long timeout = -1;
20
21 #ifdef USING_TIMERFD
22 if (epollop->timerfd >= 0) {
23 struct itimerspec is;
24 is.it_interval.tv_sec = 0;
25 is.it_interval.tv_nsec = 0;
26 if (tv == NULL) {
27 /* No timeout; disarm the timer. */
28 is.it_value.tv_sec = 0;
29 is.it_value.tv_nsec = 0;
30 } else {
31 if (tv->tv_sec == 0 && tv->tv_usec == 0) {
32 /* we need to exit immediately; timerfd can't
33 * do that. */
34 timeout = 0;
35 }
36 is.it_value.tv_sec = tv->tv_sec;
37 is.it_value.tv_nsec = tv->tv_usec * 1000;
38 }
39 /* TODO: we could avoid unnecessary syscalls here by only
40 calling timerfd_settime when the top timeout changes, or
41 when we're called with a different timeval.
42 */
43 if (timerfd_settime(epollop->timerfd, 0, &is, NULL) < 0) {
44 event_warn("timerfd_settime");
45 }
46 } else
47 #endif
48
49 /**
50 * 如果存在超时事件,则将超时时间转换为毫秒时间
51 * 如果超时时间在合法范围之外,则设置超时时间为永久等待
52 */
53
54 if (tv != NULL) {
55 timeout = evutil_tv_to_msec_(tv);
56 if (timeout < 0 || timeout > MAX_EPOLL_TIMEOUT_MSEC) {
57 /* Linux kernels can wait forever if the timeout is
58 * too big; see comment on MAX_EPOLL_TIMEOUT_MSEC. */
59 timeout = MAX_EPOLL_TIMEOUT_MSEC;
60 }
61 }
62
63 // 将event_base中有改变的事件列表都根据事件类型应用到epoll的监听上;重新设置之后,则删除改变
64 epoll_apply_changes(base);
65 event_changelist_remove_all_(&base->changelist, base);
66
67 // 等待事件触发,如果没有超时事件时(tv=null)时,timeout=-1,则为阻塞模式
68 EVBASE_RELEASE_LOCK(base, th_base_lock);
69
70 res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
71
72 EVBASE_ACQUIRE_LOCK(base, th_base_lock);
73
74 if (res == -1) {
75 if (errno != EINTR) {
76 event_warn("epoll_wait");
77 return (-1);
78 }
79
80 return (0);
81 }
82
83 event_debug(("%s: epoll_wait reports %d", __func__, res));
84 EVUTIL_ASSERT(res <= epollop->nevents);
85
86 // 根据epoll等待触发的事件列表激活事件
87 for (i = 0; i < res; i++) {
88 int what = events[i].events;
89 short ev = 0;
90 #ifdef USING_TIMERFD
91 if (events[i].data.fd == epollop->timerfd)
92 continue;
93 #endif
94
95 if (what & (EPOLLHUP|EPOLLERR)) {
96 ev = EV_READ | EV_WRITE;
97 } else {
98 if (what & EPOLLIN)
99 ev |= EV_READ;
100 if (what & EPOLLOUT)
101 ev |= EV_WRITE;
102 if (what & EPOLLRDHUP)
103 ev |= EV_CLOSED;
104 }
105
106 if (!ev)
107 continue;
108
109 // 根据事件绑定的fd,事件类型以及触发方式激活事件
110 evmap_io_active_(base, events[i].data.fd, ev | EV_ET);
111 }
112
113 /**
114 * 如果激活的事件个数等于epoll中事件总数,同时epoll中事件总数小于最大总数,
115 * 则需要创建新空间用来存储新事件,方法是每次创建的空间等于原来的2倍
116 */
117 if (res == epollop->nevents && epollop->nevents < MAX_NEVENT) {
118 /* We used all of the event space this time. We should
119 be ready for more events next time. */
120 int new_nevents = epollop->nevents * 2;
121 struct epoll_event *new_events;
122
123 new_events = mm_realloc(epollop->events,
124 new_nevents * sizeof(struct epoll_event));
125 if (new_events) {
126 epollop->events = new_events;
127 epollop->nevents = new_nevents;
128 }
129 }
130
131 return (0);
132 }
完整的处理框架如下所示:
注意:
- libevent 中,初始化阶段并不注册读 socket 的读事件,而是在注册信号阶段才会测试并注册;
- libevent 中,检查 I/O 事件是在各系统 I/O 机制的 dispatch()函数中完成的,该 dispatch()函数在 event_base_loop()函数中被调用;
8.3 evsignal_info 结构体
Libevent 中 Signal 事件的管理是通过结构体 evsig_info 完成的,结构体位于 libevent-master\evsignal-internal.h 文件中,定义如下:
1 /* Data structure for the default signal-handling implementation in signal.c
2 */
3 struct evsig_info {
4 /* Event watching ev_signal_pair[1] 监听ev_signal_pair[1]的事件,即内部信号管道监听事件*/
5 struct event ev_signal;
6 /**
7 * Socketpair used to send notifications from the signal handler
8 * 内部信号管道的两端,0的一端用来读信号,1的一端用来写信号
9 */
10 evutil_socket_t ev_signal_pair[2];
11 /* True iff we've added the ev_signal event yet. 如果我们已经增添了ev_signal则为真,即注册了外部信号事件的话*/
12 int ev_signal_added;
13 /* Count of the number of signals we're currently watching. 用来统计当前event_base到底监听了多少信号*/
14 int ev_n_signals_added;
15
16 /* Array of previous signal handler objects before Libevent started
17 * messing with them. Used to restore old signal handlers. */
18 //在libevent开始信号处理之前的原有信号句柄
19 #ifdef EVENT__HAVE_SIGACTION
20 struct sigaction **sh_old;
21 #else
22 ev_sighandler_t **sh_old;
23 #endif
24 /* Size of sh_old. 原有的信号句柄的最大个数*/
25 int sh_old_max;
26 };
下面详细介绍一下个字段的含义和作用:
1)ev_signal, 为 socket pair 的读 socket 向 event_base 注册读事件时使用的 event 结构体;
2)ev_signal_pair,socket pair 对;
3)ev_signal_added,记录 ev_signal 事件是否已经注册了;
4)ev_n_signals_added,用来统计当前event_base到底监听了多少信号
5)sh_old 记录了原来的 signal 处理函数指针,当信号 signo 注册的 event 被清空时,需要重新设置其处理函数; evsignal_info 的初始化包括,创建 socket pair,设置 ev_signal 事件(但并没有注册,而 是等到有信号注册时才检查并注册),并将所有标记置零,初始化信号的注册事件链表指针等。
8.4 注册、注销 signal 事件
注册 signal 事件是通过 evsignal_add(struct event *ev)函数完成的,libevent 对所有的信号注册同一个处理函数 evsignal_handler(),该函数将在下一段介绍,注册过程如下:
1 取得 ev 要注册到的信号 signo;
2 如果信号 signo 未被注册,那么就为 signo 注册信号处理函数 evsignal_handler();
3 如果事件 ev_signal 还没哟注册,就注册 ev_signal 事件;
4 将事件 ev 添加到 signo 的 event 链表中; 从 signo 上注销一个已注册的 signal 事件就更简单了,直接从其已注册事件的链表中移除即可。如果事件链表已空,那么就恢复旧的处理函数;
处理函数evsignal_handler()函数做的事情很简单,并通知 event_base有信号触发,需要处理,触发套接字对上面的读事件:
1 /**
2 * 这是外部信号发生时的处理函数,主要用于将信号值写入base->sig.ev_signal_pair[1],以达到通知外部信号发生的目的
3 */
4 static void __cdecl
5 evsig_handler(int sig)
6 {
7 int save_errno = errno;
8 #ifdef _WIN32
9 int socket_errno = EVUTIL_SOCKET_ERROR();
10 #endif
11 ev_uint8_t msg;
12
13 if (evsig_base == NULL) {
14 event_warnx(
15 "%s: received signal %d, but have no base configured",
16 __func__, sig);
17 return;
18 }
19
20 #ifndef EVENT__HAVE_SIGACTION
21 signal(sig, evsig_handler);
22 #endif
23
24 /* Wake up our notification mechanism */
25 /**
26 * 唤醒通知机制
27 * 使用写入内部管道的消息保存信号值
28 */
29 msg = sig;
30 #ifdef _WIN32
31 //windows环境中,向socketpair套接字对的写端写入一个字节
32 send(evsig_base_fd, (char*)&msg, 1, 0);
33 #else
34 {
35 /**
36 * 将消息写入内部管道文件描述符,注意是一个字节,因为信号使用一个字节就可以表示了
37 * evsig_base_fd在前面就已经设置了
38 */
39 int r = write(evsig_base_fd, (char*)&msg, 1);
40 (void)r; /* Suppress 'unused return value' and 'unused var' */
41 }
42 #endif
43 errno = save_errno;
44 #ifdef _WIN32
45 EVUTIL_SET_SOCKET_ERROR(socket_errno);
46 #endif
47 }
九、集成定时器事件
现在来详细分析 libevent 中 I/O 事件和 Timer 事件的集成,与 Signal 相比,Timer 事 件的集成会直观和简单很多。Libevent 对堆的调整操作做了一些优化,本节还会描述这些优化方法。
9.1 集成到事件主循环
因为系统的 I/O 机制像 select()和 epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有 I/O 事件发生,它们也保证能在 timeout 时间内返回。
那么根据所有 Timer 事件的最小超时时间来设置系统 I/O 的 timeout 时间;当系统 I/O 返回时,再激活所有就绪的 Timer 事件就可以了,这样就能将 Timer 事件完美的融合到系统 的 I/O 机制中了。
具体的代码在源文件 event.c 的 event_base_loop()中,现在就对比代码来看看这一处理方法:
1 /**
2 * 如果event_base的活跃事件数量为空并且是非阻塞模式,则获取下一个超时事件的距离超时的时间间隔,用于后台
3 * 方法用于调度超时事件,否则清空存储距离超时的时间间隔。
4 * 下文分析
5 */
6 if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
7 timeout_next(base, &tv_p);
8 } else {
9 /*
10 * if we have active events, we just poll new events
11 * without waiting.
12 * 如果还有活动事件,就不要等待,让evsel->dispatch立即返回
13 */
14 evutil_timerclear(&tv);
15 }
16
17 /* If we have no events, we just exit 没有设置EVLOOP_NO_EXIT_ON_EMPTY标志,当没有激活事件发生时,就退出*/
18 if (0==(flags&EVLOOP_NO_EXIT_ON_EMPTY) &&
19 !event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
20 event_debug(("%s: no events registered.", __func__));
21 retval = 1;
22 goto done;
23 }
24
25 // 将下一次激活事件队列中的事件都移动到激活队列中
26 event_queue_make_later_events_active(base);
27
28 // 清空event_base中时间,防止误用
29 clear_time_cache(base);
30
31 // 调用后台方法的dispatch方法,如果后台方法是epoll的话就调用epoll_dispatch
32 res = evsel->dispatch(base, tv_p);
33
34 if (res == -1) {
35 event_debug(("%s: dispatch returned unsuccessfully.",
36 __func__));
37 retval = -1;
38 goto done;
39 }
40
41 // 更新当前event_base中缓存的时间,因为这是在执行后台调度方法之后的时间,可以用来作为超时事件的参考时间
42 update_time_cache(base);
43
44 // 主要是从超时事件最小堆中取出超时事件,并将超时事件放入激活队列
45 timeout_process(base);
9.2 Timer 小根堆
Libevent 使用堆来管理 Timer 事件,其 key 值就是事件的超时时间,源代码位于文件 min_heap.h 中。
所有的数据结构书中都有关于堆的详细介绍,向堆中插入、删除元素时间复杂度都是 O(lgN),N 为堆中元素的个数,而获取最小 key 值(小根堆)的复杂度为 O(1)。堆是一个完 全二叉树,基本存储方式是一个数组。 Libevent 实现的堆还是比较轻巧的,虽然我不喜欢这种编码方式(搞一些复杂的表达 式)。轻巧到什么地方呢,就以插入元素为例,源代码如下:
1 int min_heap_push_(min_heap_t* s, struct event* e)
2 {
3 if (min_heap_reserve_(s, s->n + 1))
4 return -1;
5 min_heap_shift_up_(s, s->n++, e);
6 return 0;
7 }
上浮操作:
1 void min_heap_shift_up_(min_heap_t* s, unsigned hole_index, struct event* e)
2 {
3 unsigned parent = (hole_index - 1) / 2;
4 while (hole_index && min_heap_elem_greater(s->p[parent], e))
5 {
6 (s->p[hole_index] = s->p[parent])->ev_timeout_pos.min_heap_idx = hole_index;
7 hole_index = parent;
8 parent = (hole_index - 1) / 2;
9 }
10 (s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
11 }
而 libevent 的 heap 代码对这一过程做了优化,在插入新元素时,只是为新元素预留了 一个位置 hole(初始时 hole 位于数组尾部),但并不立刻将新元素插入到 hole 上,而是不断向上调整 hole 的值,将父节点向下调整,最后确认 hole 就是新元素的所在位置时,才会真正的将新元素插入到 hole 上,因此在调整过程中就比上面的代码少了一次赋值的操作。
由于每次调整都少做一次赋值操作,在调整路径比较长时,调整效率会比第一种有所 提高。libevent 中的 min_heap_shift_up_()函数就是上面逻辑的具体实现,对应的向下调整函 数是 min_heap_shift_down_()。 举个例子,向一个小根堆 3, 5, 8, 7, 12 中插入新元素 2,使用第一中典型的代码逻辑, 其调整过程如下图所示:
使用 libevent 中的堆调整逻辑,调整过程如下图所示:
对于删除和元素修改操作,也遵从相同的逻辑,就不再罗嗦了。
十、支持 I/O 多路复用技术
Libevent 的核心是事件驱动、同步非阻塞,为了达到这一目标,必须采用系统提供的 I/O 多路复用技术,而这些在 Windows、Linux、Unix 等不同平台上却各有不同,如何能提供优雅而统一的支持方式,是首要关键的问题,这其实不难,本节就来分析一下。
10.1 统一的关键
Libevent支持多种I/O多路复用技术的关键就在于结构体eventop,这个结构体前面也曾 提到过,它的成员是一系列的函数指针, 定义在event-internal.h文件中:
1 /** Structure to define the backend of a given event_base. */
2 struct eventop {
3 /** The name of this backend. 后台方法名字,即epoll,select,poll等*/
4 const char *name;
5 /** Function to set up an event_base to use this backend. It should
6 * create a new structure holding whatever information is needed to
7 * run the backend, and return it. The returned pointer will get
8 * stored by event_init into the event_base.evbase field. On failure,
9 * this function should return NULL. */
10 /**
11 * 配置libevent句柄event_base使用当前后台方法;他应该创建新的数据结构,
12 * 隐藏了后台方法运行所需的信息,然后返回这些信息的结构体,为了支持多种
13 * 结构体,因此返回void*;返回的指针将保存在event_base.evbase中;如果失败,
14 * 将返回NULL
15 */
16 void *(*init)(struct event_base *);
17 /** Enable reading/writing on a given fd or signal. 'events' will be
18 * the events that we're trying to enable: one or more of EV_READ,
19 * EV_WRITE, EV_SIGNAL, and EV_ET. 'old' will be those events that
20 * were enabled on this fd previously. 'fdinfo' will be a structure
21 * associated with the fd by the evmap; its size is defined by the
22 * fdinfo field below. It will be set to 0 the first time the fd is
23 * added. The function should return 0 on success and -1 on error.
24 */
25 /**
26 * 使给定的文件描述符或者信号变得可读或者可写。’events’将是我们尝试添加的
27 * 事件类型:一个或者更多的EV_READ,EV_WRITE,EV_SIGNAL,EV_ET。’old’是这些事件
28 * 先前的事件类型;’fdinfo’将是fd在evmap中的辅助结构体信息,它的大小由下面的
29 * fdinfo_len给出。fd第一次添加时将设置为0.成功则返回0,失败则返回-1
30 */
31 int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
32 /** As "add", except 'events' contains the events we mean to disable. */
33 // 删除事件
34 int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
35 /** Function to implement the core of an event loop. It must see which
36 added events are ready, and cause event_active to be called for each
37 active event (usually via event_io_active or such). It should
38 return 0 on success and -1 on error.
39 */
40 /**
41 * event_loop实现的核心代码。他必须察觉哪些添加的事件已经准备好,然后触发每个
42 * 活跃事件都被调用(通常是通过event_io_active或者类似这样)。成功返回0,失败则-1
43 */
44 int (*dispatch)(struct event_base *, struct timeval *);
45 /** Function to clean up and free our data from the event_base. */
46 // 清除event_base并释放数据
47 void (*dealloc)(struct event_base *);
48 /** Flag: set if we need to reinitialize the event base after we fork.
49 */
50 // 在执行fork之后是否需要重新初始化的标识位
51 int need_reinit;
52 /** Bit-array of supported event_method_features that this backend can
53 * provide. */
54
55 // 后台方法可以提供的特征
56 // enum event_method_feature {
57 // 边沿触发
58 // EV_FEATURE_ET = 0x01,
59 // 要求事后台方法在调度很多事件时大约为O(1)操作,select和poll无法提供这种特征,
60 // 这两种方法具有N个事件时,可以提供O(N)操作
61 // EV_FEATURE_O1 = 0x02,
62
63 // 后台方法可以处理各种文件描述符,而不仅仅是sockets
64 // EV_FEATURE_FDS = 0x04,
65 /** Require an event method that allows you to use EV_CLOSED to detect
66 * connection close without the necessity of reading all the pending data.
67 *
68 * Methods that do support EV_CLOSED may not be able to provide support on
69 * all kernel versions.
70 **/
71 // 要求后台方法允许使用EV_CLOSED特征检测链接是否中断,而不需要读取
72 // 所有未决数据;但是不是所有内核都能提供这种特征
73 // EV_FEATURE_EARLY_CLOSE = 0x08
74 // };
75
76 enum event_method_feature features;
77 /** Length of the extra information we should record for each fd that
78 has one or more active events. This information is recorded
79 as part of the evmap entry for each fd, and passed as an argument
80 to the add and del functions above.
81 */
82 /**
83 * 应该为每个文件描述符保留的额外信息长度,额外信息可能包括一个或者多个
84 * 活跃事件。这个信息是存储在每个文件描述符的evmap中,然后通过参数传递
85 * 到上面的add和del函数中。
86 */
87 size_t fdinfo_len;
88 };
在 libevent 中,每种 I/O demultiplex 机制的实现都必须提供这五个函数接口,来完成自身的初始化、销毁释放;对事件的注册、注销和分发。 比如对于 epoll,libevent 实现了 5 个对应的接口函数,并在初始化时并将 eventop 的 5 个函数指针指向这 5 个函数,那么程序就可以使用 epoll 作为 I/O demultiplex 机制了。
10.2 设置 I/O demultiplex 机制
Libevent 把所有支持的 I/O demultiplex 机制存储在一个全局静态数组 eventops 中,并在 初始化时选择使用何种机制,数组内容根据优先级顺序声明如下:
1 /* Array of backends in order of preference. */
2 /**
3 * 后台方法
4 * libevnt的底层实现,优先级从上到下依次从高到低
5 * 后台方法的全局静态数组
6 */
7 static const struct eventop *eventops[] = {
8 #ifdef EVENT__HAVE_EVENT_PORTS
9 &evportops,
10 #endif
11 #ifdef EVENT__HAVE_WORKING_KQUEUE
12 &kqops,
13 #endif
14 #ifdef EVENT__HAVE_EPOLL
15 &epollops,
16 #endif
17 #ifdef EVENT__HAVE_DEVPOLL
18 &devpollops,
19 #endif
20 #ifdef EVENT__HAVE_POLL
21 &pollops,
22 #endif
23 #ifdef EVENT__HAVE_SELECT
24 &selectops,
25 #endif
26 #ifdef _WIN32
27 &win32ops,
28 #endif
29 NULL
30 };
然后 libevent 根据系统配置和编译选项决定使用哪一种 I/O demultiplex 机制,这段代码 在函数 event_base_new()中:
1 /**
2 * 遍历静态全局变量eventops,对选择的后台方法进行初始化
3 * eventops定义:
4 * libevent源码分析(3)--2.1.8--结构体struct event_base和struct eventop
5 * 我自己测试结果是,默认选择使用epoll方法,而测试环境支持的后台方法包括
6 * epoll,poll,select
7 * 下面看一下,程序是怎样选出epoll方法,跳过poll和select
8 */
9 for (i = 0; eventops[i] && !base->evbase; i++) {
10 if (cfg != NULL) {
11 /* determine if this backend should be avoided */
12 /**
13 * 如果方法已经屏蔽,则跳过去,继续遍历下一个方法
14 * 默认情况下,是不屏蔽任何后台方法,除非通过编译选项控制或者使用API屏蔽
15 */
16 if (event_config_is_avoided_method(cfg,
17 eventops[i]->name))
18 continue;
19
20 /**
21 * 如果后台方法的工作模式特征和配置的工作模式不同,则跳过去
22 * 查看libevent源码中epoll.c、poll.c、select.c文件相应静态全局变量的方法定义,
23 * 发现默认情况下,各个后台方法的特征如下:
24 * epoll方法的特征是 :EV_FEATURE_ET|EV_FEATURE_O1|EV_FEATURE_EARLY_CLOSE
25 * poll方法的特征是:EV_FEATURE_FDS
26 * select方法的特征是:EV_FEATURE_FDS
27 * 默认情况下,cfg中require_features是EV_FEATURE_ET,所以poll和select都会跳过去,只有epoll执行初始化
28 */
29 if ((eventops[i]->features & cfg->require_features)
30 != cfg->require_features)
31 continue;
32 }
33
34 /* also obey the environment variables */
35 //如果检查环境变量,并发现OS环境不支持的话,也会跳过去
36 if (should_check_environment &&
37 event_is_method_disabled(eventops[i]->name))
38 continue;
39
40 /**
41 * 下面两步正确情况下只会选择一个后台方法,也只会执行一次
42 * 保存后台方法句柄,实际是静态全局变量数组成员,具体定义在每种方法文件中定义
43 */
44 base->evsel = eventops[i];
45
46 /**
47 * 调用相应后台方法的初始化函数进行初始化,这个就用到结构体
48 * struct eventop中定义的init,具体方法的init实现需要查看每种方法自己的定义
49 * 以epoll为例,epoll相关实现都在epoll.c中
50 * 后面会分析epoll的初始化函数
51 * 如果使用的是epoll方法的话,实际上就是调用epoll_init函数在epoll.c中
52 *
53 * 在这里面会创建信号通知的监听事件和通知管道的创建
54 */
55 base->evbase = base->evsel->init(base);
56 }
可以看出,libevent 在编译阶段选择系统的 I/O demultiplex 机制,而不支持在运行阶段根据配置再次选择。以 Linux 下面的 epoll 为例,实现在源文件 epoll.c 中,eventops 对象 epollops 定义如下:
1 /**
2 * 参考前文中在event.c中定义的静态全局变量数组:eventops,
3 * eventops中存储就是这个epollops。
4 * 此处需要注意的是内部信号通知机制的实现,是通过管道传递外部信号的,为何不能像IO事件绑定文件描述符?
5 * 因为信号是全局的,无法真对某个事件进行绑定,只能通过函数捕捉信号,然后通过管道通知event_base来实现。
6 */
7
8 const struct eventop epollops = {
9 "epoll",
10 epoll_init,
11 epoll_nochangelist_add,
12 epoll_nochangelist_del,
13 epoll_dispatch,
14 epoll_dealloc,
15 1, /* need reinit */
16 EV_FEATURE_ET|EV_FEATURE_O1|EV_FEATURE_EARLY_CLOSE,
17 0
18 };
变量 epollops 中的函数指针具体声明如下,注意到其返回值和参数都和 eventop 中的定 义严格一致,这是函数指针的语法限制。
十一、时间管理
为了支持定时器,Libevent 必须和系统时间打交道,这一部分的内容也比较简单,主要涉及到时间的加减辅助函数、时间缓存、时间校正和定时器堆的时间值调整等。下面就结合源代码来分析一下。
11.1 初始化检测
Libevent 在初始化时会检测系统时间的类型,通过调用函数 detect_monotonic()完成,它 通过调用 clock_gettime()来检测系统是否支持 monotonic 时钟类型:
1 /**
2 * 配置单调递增的时间,默认情况下,会使用EV_MONOTONIC模式获取系统时间,
3 * 并将event_base的monotonic_clock模式设置为EV_MONOTONIC;
4 * monotonic时间是单调递增的时间,不受系统修改时间影响,对于计算两个时间点之间的时间比较精确;
5 * real时间是系统时间,受系统时间影响
6 *
7 * POSIX clock_gettime接口提供获得monotonic时间的方式。CLOCK_MONOTONIC基本上都是支持的;
8 * linux也提供CLOCK_MONOTONIC_COARSE模式,大约1-4毫秒的准确性。
9 * 所有平台上,CLOCK_MONOTONIC实际上是单调递增的。
10 */
11
12 int
13 evutil_configure_monotonic_time_(struct evutil_monotonic_timer *base,
14 int flags)
15 {
16 /* CLOCK_MONOTONIC exists on FreeBSD, Linux, and Solaris. You need to
17 * check for it at runtime, because some older kernel versions won't
18 * have it working. */
19 /**
20 * CLOCK_MONOTONIC在FreeBSD,Linux,Solaris一般是支持的。运行时你需要进行检查,因为
21 * 一些老的内核版本可能不支持
22 */
23 /**
24 * 如果定义了CLOCK_MONOTONIC_COARSE编译选项,则检查event_base工作模式是否选择了EV_MONOT_PRECISE;
25 * 如果选择了EV_MONOT_PRECISE,则设置标志位precise,表明选择使用准确的时间模式;
26 * 默认情况下,flags采用的是EV_MONOT_PRECISE,所以precise=EV_MONOT_PRECISE=1
27 */
28 #ifdef CLOCK_MONOTONIC_COARSE
29 const int precise = flags & EV_MONOT_PRECISE;
30 #endif
31
32 /**
33 * 设置fallback标志位,查看是否为EV_MONOT_FALLBACK=2模式
34 * 默认情况下,flags采用的是EV_MONOT_PRECISE,所以fallback为0
35 */
36
37 const int fallback = flags & EV_MONOT_FALLBACK;
38 struct timespec ts;
39
40 #ifdef CLOCK_MONOTONIC_COARSE
41 if (CLOCK_MONOTONIC_COARSE < 0) {
42 /* Technically speaking, nothing keeps CLOCK_* from being
43 * negative (as far as I know). This check and the one below
44 * make sure that it's safe for us to use -1 as an "unset"
45 * value. */
46 event_errx(1,"I didn't expect CLOCK_MONOTONIC_COARSE to be < 0");
47 }
48
49 /**
50 * 如果既没有选择EV_MONOT_PRECISE模式,也没有选择EV_MONOT_FALLBACK模式,则使用
51 * CLOCK_MONOTONIC_COARSE获取当前系统时间
52 * 默认情况下选择的是EV_MONOT_PRECISE,所以不走此分支
53 */
54 if (! precise && ! fallback) {
55 if (clock_gettime(CLOCK_MONOTONIC_COARSE, &ts) == 0) {
56 base->monotonic_clock = CLOCK_MONOTONIC_COARSE;
57 return 0;
58 }
59 }
60 #endif
61
62 /**
63 * 如果没有选择EV_MONOT_FALLBACK,则以CLOCK_MONOTONIC模式获取系统时间,并将
64 * event_base的monotonic_clock模式设置为CLOCK_MONOTONIC;
65 * 默认情况下,选择的是EV_MONOT_PRECISE,所以此分支执行,
66 * 此函数最终的结果是获取CLOCK_MONOTONIC模式的时间,并将event_base的时钟模式设置为
67 * CLOCK_MONOTONIC模式
68 */
69 if (!fallback && clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
70 base->monotonic_clock = CLOCK_MONOTONIC;
71 return 0;
72 }
73
74 if (CLOCK_MONOTONIC < 0) {
75 event_errx(1,"I didn't expect CLOCK_MONOTONIC to be < 0");
76 }
77
78 base->monotonic_clock = -1;
79 return 0;
80 }
Monotonic 时间指示的是系统从 boot 后到现在所经过的时间,如果系统支持 Monotonic 时间就将全局变量 use_monotonic 设置为 1,设置 use_monotonic 到底有什么用,这个在后面 说到时间校正时就能看出来了。
11.2 时间缓存
结构体 event_base 中的 tv_cache,用来记录时间缓存。这个还要从函数 gettime()说起, 先来看看该函数的代码:
1 /**
2 * 将tp设置为base的当前时间。必需在base上加锁;如果有缓存的时间,可以反回缓存的时间。
3 * 否则,需要调用clock_gettime或者gettimeofday获取合适的时间;
4 */
5 static int
6 gettime(struct event_base *base, struct timeval *tp)
7 {
8 EVENT_BASE_ASSERT_LOCKED(base);
9
10 // 首先查看base中是否有缓存的时间,如果有,直接使用缓存时间,然后返回即可
11 if (base->tv_cache.tv_sec) {
12 *tp = base->tv_cache;
13 return (0);
14 }
15
16 /**
17 * 使用monotonic_timer获取当前时间,二选一:
18 * CLOCK_MONOTONIC_COARSE 和 CLOCK_MONOTONIC
19 * 默认情况下是 CLOCK_MONOTONIC模式
20 */
21 if (evutil_gettime_monotonic_(&base->monotonic_timer, tp) == -1) {
22 return -1;
23 }
24
25 /**
26 * 查看是否需要更新缓存的时间
27 * 如果上次更新时间的时间点距离当前时间点的间隔超过CLOCK_SYNC_INTERVAL,则需要更新
28 */
29 if (base->last_updated_clock_diff + CLOCK_SYNC_INTERVAL
30 < tp->tv_sec) {
31 struct timeval tv;
32 // 使用gettimeofday获取当前系统real时间
33 evutil_gettimeofday(&tv,NULL);
34 // 将当前系统real时间和monotonic时间做差,存到base中
35 evutil_timersub(&tv, tp, &base->tv_clock_diff);
36 // 保存当前更新的时间点
37 base->last_updated_clock_diff = tp->tv_sec;
38 }
39
40 return 0;
41 }
如果 tv_cache 已经设置,那么就直接使用缓存的时间;否则需要再次执行系统调用获取系统时间。 函数 evutil_gettimeofday()用来获取当前系统时间,在 Linux 下其实就是系统调用 gettimeofday();Windows 没有提供函数 gettimeofday,而是通过调用_ftime()来完成的。 在每次系统事件循环中,时间缓存 tv_cache 将会被相应的清空和设置,再次来看看下 面 event_base_loop 的主要代码逻辑:
1 // 清空当前event_base中的时间,防止误用
2 clear_time_cache(base);
3
4 // 如果event_base中有信号事件,则需要设置
5 if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)
6 evsig_set_base_(base);
7
8 done = 0;
9
10 #ifndef EVENT__DISABLE_THREAD_SUPPORT
11 base->th_owner_id = EVTHREAD_GET_ID();
12 #endif
13
14 base->event_gotterm = base->event_break = 0;
15
16 while (!done) {
17 base->event_continue = 0;
18 base->n_deferreds_queued = 0;
19
20 /* Terminate the loop if we have been asked to */
21 // 每次loop时,需要判定是否别的地方已经设置了终止或者退出的标志位
22 if (base->event_gotterm) {
23 break;
24 }
25
26 if (base->event_break) {
27 break;
28 }
29
30 tv_p = &tv;
31
32 /**
33 * 如果event_base的活跃事件数量为空并且是非阻塞模式,则获取下一个超时事件的距离超时的时间间隔,用于后台
34 * 方法用于调度超时事件,否则清空存储距离超时的时间间隔。
35 * 下文分析
36 */
37 if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
38 timeout_next(base, &tv_p);
39 } else {
40 /*
41 * if we have active events, we just poll new events
42 * without waiting.
43 * 如果还有活动事件,就不要等待,让evsel->dispatch立即返回
44 */
45 evutil_timerclear(&tv);
46 }
47
48 /* If we have no events, we just exit 没有设置EVLOOP_NO_EXIT_ON_EMPTY标志,当没有激活事件发生时,就退出*/
49 if (0==(flags&EVLOOP_NO_EXIT_ON_EMPTY) &&
50 !event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
51 event_debug(("%s: no events registered.", __func__));
52 retval = 1;
53 goto done;
54 }
55
56 // 将下一次激活事件队列中的事件都移动到激活队列中
57 event_queue_make_later_events_active(base);
58
59 // 清空event_base中时间,防止误用---时间点1
60 clear_time_cache(base);
61
62 // 调用后台方法的dispatch方法,如果后台方法是epoll的话就调用epoll_dispatch
63 res = evsel->dispatch(base, tv_p);
64
65 if (res == -1) {
66 event_debug(("%s: dispatch returned unsuccessfully.",
67 __func__));
68 retval = -1;
69 goto done;
70 }
71
72 // 更新当前event_base中缓存的时间,因为这是在执行后台调度方法之后的时间,可以用来作为超时事件的参考时间---时间点2
73 update_time_cache(base);
74
75 // 主要是从超时事件最小堆中取出超时事件,并将超时事件放入激活队列
76 timeout_process(base);
77
78 /**
79 * 如果激活队列不为空,则处理激活的事件
80 * 否则,如果模式为非阻塞,则退出loop
81 */
82 if (N_ACTIVE_CALLBACKS(base)) {
83 //通过event_process_active函数来处理已经激活的event
84 int n = event_process_active(base);
85 if ((flags & EVLOOP_ONCE)
86 && N_ACTIVE_CALLBACKS(base) == 0
87 && n != 0)//如果没有激活事件就等待,至少处理一次激活时间
88 done = 1;
89 } else if (flags & EVLOOP_NONBLOCK)//没有激活事件立即退出
90 done = 1;
91 }
92 event_debug(("%s: asked to terminate loop.", __func__));
93
94 done:
95 // 退出时也要清空时间缓存
96 clear_time_cache(base);
97 base->running_loop = 0;
98
99 EVBASE_RELEASE_LOCK(base, th_base_lock);
100
101 return (retval);
102 }
时间 event_tv 指示了 dispatch()上次返回,也就是 I/O 事件就绪时的时间,第一次进入循环时,由于 tv_cache 被清空,因此 gettime()执行系统调用获取当前系统时间;而后将会更新为 tv_cache 指示的时间。 时间 tv_cache 在 dispatch()返回后被设置为当前系统时间,因此它缓存了本次 I/O 事件就绪时的时间(event_tv)。 从代码逻辑里可以看出 event_tv 取得的是 tv_cache 上一次的值,因此 event_tv 应该小于tv_cache 的值。 设置时间缓存的优点是不必每次获取时间都执行系统调用,这是个相对费时的操作;在上面标注的时间点 2 到时间点 1 的这段时间(处理就绪事件时),调用 gettime()取得的都是 tv_cache 缓存的时间。
十二 让 libevent 支持多线程
12.1 暴力抢占
那么第一节中使用的多线程方法相当下面的流程:
1 当时你正在做事,比如在写文档;
2 你的头找到了一个任务,要指派给你,比如帮他搞个 PPT,哈;
3 头命令你马上搞 PPT,你这是不得不停止手头的工作,把 PPT 搞定了再接着写文档; …
12.2 纯粹的消息通知机制
那么基于纯粹的消息通知机制的多线程方式就像下面这样:
1 当时你正在写文档;
2 你的头找到了一个任务,要指派给你,帮他搞个 PPT;
3 头发个消息到你信箱,有个 PPT 要帮他搞定,这时你并不鸟他;
4 你写好文档,接着检查消息发现头有个 PPT 要你搞定,你开始搞 PPT;
…
第一种的好处是消息可以立即得到处理,但是很方法很粗暴,你必须立即处理这个消息, 所以你必须处理好切换问题,省得把文档上的内容不小心写到 PPT 里。在操作系统的进程 通信中,消息队列(消息信箱)都是操作系统维护的,你不必关心。
第二种的优点是通过消息通知,切换问题省心了,不过消息是不能立即处理的(基于消 息通知机制,这个总是难免的),而且所有的内容都通过消息发送,比如 PPT 的格式、内容 等等信息,这无疑增加了通信开销。
12.3 消息通知+同步层
有个折中机制可以减少消息通信的开销,就是提取一个同步层,还拿上面的例子来说, 你把工作安排都存放在一个工作队列中,而且你能够保证“任何人把新任务扔到这个队列”, “自己取出当前第一个任务”等这些操作都能够保证不会把队列搞乱(其实就是个加锁的队 列容器)。
再来看看处理过程和上面有什么不同:
1 当时你正在写文档;
2 你的头找到了一个任务,要指派给你,帮他搞个 PPT;
2 头有个 PPT 要你搞定,他把任务 push 到你的工作队列中,包括了 PPT 的格式、内容 等信息;
3 头发个消息(一个字节)到你信箱,有个 PPT 要帮他搞定,这时你并不鸟他;
4 你写好文档,发现有新消息(这预示着有新任务来了),检查工作队列知道头有个 PPT 要你搞定,你开始搞 PPT;
…
工作队列其实就是一个加锁的容器(队列、链表等等),这个很容易实现实现;而消息 通知仅需要一个字节,具体的任务都 push 到了在工作队列中,因此想比 12.2 减少了不少通 信开销。 多线程编程有很多陷阱,线程间资源的同步互斥不是一两句能说得清的,而且出现 bug 很难跟踪调试;这也有很多的经验和教训,因此如果让我选择,在绝大多数情况下都会选择 机制 3 作为实现多线程的方法。
12.3 例子——memcached
Memcached 中的网络部分就是基于 libevent 完成的,其中的多线程模型就是典型的消息 通知+同步层机制。下面的图足够说明其多线程模型了,其中有详细的文字说明。
参考文章
libevent 源码深度剖析---张亮
本文来自博客园,作者:Mr-xxx,转载请注明原文链接:https://www.cnblogs.com/MrLiuZF/p/15059409.html