这两天没事,看了一下Memcached和libevent的源码,做个小总结。
1、入门
1.1、概述
Libevent是一个用于开发可扩展性网络服务器的基于事件驱动(event-driven)模型的网络库。Libevent有几个显著的亮点:
(1)事件驱动(event-driven),高性能;
(2)轻量级,专注于网络,不如 ACE 那么臃肿庞大;
(3)源代码相当精炼、易读;
(4)跨平台,支持 Windows、Linux、*BSD和 Mac Os;
(5)支持多种 I/O多路复用技术, epoll、poll、dev/poll、select 和kqueue 等;
(6)支持 I/O,定时器和信号等事件;
(7)注册事件优先级;
Libevent 已经被广泛的应用,作为底层的网络库;比如 memcached、 Vomi t、 Nylon、 Netchat等等。
1.2、一个简单示例
1
int
lasttime;
2
3
static
void
4
timeout_cb(
int
fd,
short
event
,
void
*
arg)
5
{
6
struct
timeval tv;
7
struct
event
*
timeout
=
arg;
8
int
newtime
=
time(NULL);
9
10
//
printf("%s: called at %d: %d\n", __func__, newtime,
11
printf(
"
%s: called at %d: %d\n
"
,
"
timeout_cb
"
, newtime,
12
newtime
-
lasttime);
13
lasttime
=
newtime;
14
15
evutil_timerclear(
&
tv);
16
tv.tv_sec
=
2
;
17
//
重新注册event
18
event_add(timeout,
&
tv);
19
}
20
21
int
22
main (
int
argc,
char
**
argv)
23
{
24
struct
event
timeout;
25
struct
timeval tv;
26
27
/*
Initalize the event library
*/
28
//
初始化event环境
29
event_init();
30
31
/*
Initalize one event
*/
32
//
设置事件
33
evtimer_set(
&
timeout, timeout_cb,
&
timeout);
34
35
evutil_timerclear(
&
tv);
36
tv.tv_sec
=
2
;
37
//
注册事件
38
event_add(
&
timeout,
&
tv);
39
40
lasttime
=
time(NULL);
41
42
//
等待,分发,处理事件
43
event_dispatch();
44
45
return
(
0
);
46
}
这是一个简单的基于libevent的定时器程序,运行结果:
用libevent编程非常简单,只需要调用event_init初始化环境,然后调用event_add注册相应的事件,接着调用event_dispatch等待并处理相应的事件即可。
调用event_add注册事件时,设置其回调函数。Libevent检测到事件发生时,便会调用事件对应的回调用函数,执行相关的业务逻辑。
1.3、源代码结构
Libevent 的源代码虽然都在一层文件夹下面,但是其代码分类还是相当清晰的,主要可分为头文件、内部使用的头文件、辅助功能函数、日志、libevent 框架、对系统 I/O 多路复用机制的封装、信号管理、定时事件管理、缓冲区管理、基本数据结构和基于 libevent的两个实用库等几个部分,有些部分可能就是一个源文件。
(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 查询库;
2、核心对象
结构体event和event_base是libevent的两个核心数据结构,前者代表一个事件对象,后者代表整个事件处理框架。
2.1、event(事件)
代码
1
//
event.h
2
struct
event
{
3
TAILQ_ENTRY (
event
) ev_next;
//
已注册事件链表
4
TAILQ_ENTRY (
event
) ev_active_next;
//
就绪事件链表
5
TAILQ_ENTRY (
event
) ev_signal_next;
//
signal链表
6
unsigned
int
min_heap_idx;
/*
for managing timeouts,事件在堆中的下标
*/
7
8
struct
event_base
*
ev_base;
9
10
int
ev_fd;
//
对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号
11
short
ev_events;
//
event关注的事件类型
12
short
ev_ncalls;
//
事件就绪执行时,调用 ev_callback 的次数
13
short
*
ev_pncalls;
/*
Allows deletes in callback
*/
14
15
struct
timeval ev_timeout;
//
timout事件的超时值
16
17
int
ev_pri;
/*
smaller numbers are higher priority,优先级
*/
18
19
void
(
*
ev_callback)(
int
,
short
,
void
*
arg);
//
回调函数
20
void
*
ev_arg;
//
回调函数的参数
21
22
int
ev_res;
/*
result passed to event callback
*/
23
int
ev_flags;
//
event的状态
24
};
25
Libevent通过event对象将I/O事件、信号事件和定时器事件封装,从而统一处理,这也是libevent的精妙所有。
各个字段的具体含义:
(1) ev_events:event关注的事件类型,它可以是以下3种类型:
I/O事件: EV_WRITE和EV_READ
定时事件:EV_TIMEOUT
信号: EV_SIGNAL
辅助选项:EV_PERSIST,表明是一个永久事件
libevent中的定义为:
#define EV_TIMEOUT 0x01
#define EV_READ 0x02
#define EV_WRITE 0x04
#define EV_SIGNAL 0x08
#define EV_PERSIST 0x10 /* Persistant event */
(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指向事件框架实例。
(5)ev_fd,对于 I/O事件,是绑定的文件描述符;对于 signal 事件,是事件对应的信号;
(6)eb_flags:libevent 用于标记 event信息的字段,表明事件当前的状态,可能的值有:
#define EVLIST_TIMEOUT 0x01 // event在time堆中
#define EVLIST_INSERTED 0x02 // event在已注册事件链表中
#define EVLIST_SIGNAL 0x04 // 未见使用
#define EVLIST_ACTIVE 0x08 // event在激活链表中
#define EVLIST_INTERNAL 0x10 // 内部使用标记
#define EVLIST_INIT 0x80 // event 已被初始化
2.2、event_base(事件处理框架)
代码
1
//
evenet_internal.h
2
struct
event_base {
3
const
struct
eventop
*
evsel;
//
底层具体I/O demultiplex操作函数集
4
void
*
evbase;
5
int
event_count;
/*
counts number of total events,总的事件数量
*/
6
int
event_count_active;
/*
counts number of active events,就绪事件数量
*/
7
8
int
event_gotterm;
/*
Set to terminate loop
*/
9
int
event_break;
/*
Set to terminate loop immediately
*/
10
11
/*
active event management
*/
12
//
就绪事件链表数组
13
struct
event_list
**
activequeues;
14
int
nactivequeues;
//
就绪事件队列个数
15
16
/*
signal handling info
*/
17
struct
evsignal_info sig;
//
用于管理信号
18
19
struct
event_list eventqueue;
//
注册事件队列
20
struct
timeval event_tv;
21
22
struct
min_heap timeheap;
//
管理定时器的小根堆
23
struct
timeval tv_cache;
//
记录时间缓存
24
};
(1)evsel:libevent 支持Linux、Windows等多种平台,也支持epoll、poll、select、kqueue等多种I/O多路复用模型。如果把 event_init、event_add看成高层抽象的统一事件操作接口,则evsel为这些函数在底层具体的I/O demultiplex的对应的操作函数集。eventop为函数指针的集合:
代码
1
struct
eventop {
2
const
char
*
name;
3
void
*
(
*
init)(
struct
event_base
*
);
4
int
(
*
add)(
void
*
,
struct
event
*
);
5
int
(
*
del)(
void
*
,
struct
event
*
);
6
int
(
*
dispatch)(
struct
event_base
*
,
void
*
,
struct
timeval
*
);
7
void
(
*
dealloc)(
struct
event_base
*
,
void
*
);
8
/*
set if we need to reinitialize the event base
*/
9
int
need_reinit;
10
};
11
在初始化函数event_base_new中,libevent将evsel指向全局数组eventops的具体元素:
代码
2.3、主要函数
2.3.1、event_int(初始化libevent实例)
struct event_base *
event_init(void)
初始化事件处理框架实例,内部调用event_base_new。
event_base_new的主要逻辑:
代码
1
struct
event_base
*
2
event_base_new(
void
)
3
{
4
5
//
初始化小根堆
6
min_heap_ctor(
&
base
->
timeheap);
7
8
//
初始化注册事件队列
9
TAILQ_INIT(
&
base
->
eventqueue);
10
11
for
(i
=
0
; eventops[i]
&&
!
base
->
evbase; i
++
) {
12
//
I/O demultiplex机制实例
13
base
->
evsel
=
eventops[i];
14
15
//
初始化I/O demultiplex实例(参见win32_init)
16
base
->
evbase
=
base
->
evsel
->
init(
base
);
17
}
18
19
//
分配1个就绪事件队列
20
event_base_priority_init(
base
,
1
);
21
22
}
2.3.2、event_add(注册事件)
//注册事件
int
event_add(struct event *ev, const struct timeval *tv)
该函数主要将事件ev加入到事件框架event_base的注册事件链表base->eventqueue。
2.3.3、event_del(删除事件)
//删除事件
int
event_del(struct event *ev)
该函数主要将事件ev从相应的链表上删除。
2.3.4、event_set(设置事件)
代码
/*
设置event对象
**ev:事件对象
**fd:事件对应的文件描述符或信号,对于定时器设为-1
**events:事件类型,比如 EV_READ,EV_PERSIST, EV_WRITE, EV_SIGNAL
**callback:事件的回调函数
**arg:回调函数参数
*/
void
event_set(
struct
event
*
ev,
int
fd,
short
events,
void
(
*
callback)(
int
,
short
,
void
*
),
void
*
arg)
在将事件注册事件处理框架之前,应该先调用event_set对事件进行相关设置。
2.4、libevent对event的管理
event结构有3个链表结点域和一个小根堆索引,libevent通过3个链表和一个小根堆对I/O事件、signal事件和timer事件进行管理。
对于I/O事件,通过event_add将其加入event_base的注册事件链表eventqueue ;就绪时会加入event_base的就绪链表activequeues[];
对于timer事件,event_add将其加入到event_base的小根堆timeheap;
Signale 事件的管理相对复杂些,event_add将其加入到注册事件链表,同时,event_add内部会调用I/O demultiplex的add函数(对于I/O事件也一样),比如epoll_add。而add函数又会调用evsignal_add将其加入到 evsignal_info的evsigevents[signo]链表(关于signal,后面会详细介绍)。
3、事件处理框架主循环
Libevent将I/O事件、signal事件和timer事件用统一的模型进行处理,这是非常精妙的。libevent主循环函数不断检测注册事件,如果有事件发生,则将其放入就绪链表,并调用事件的回调函数,完成业务逻辑处理。
3.1、event_dispatch
//事件处理主循环
int
event_dispatch(void)
这是呈现给外部的接口,它的实现很简单,即调用event_loop,而event_loop调用event_base_loop,event_base_loop完成实际的主循环逻辑。
3.2、event_base_loop
主要算法:
1
done
=
0
;
2
while
(
!
done) {
3
4
/*
如果没有就绪事件,根据timer heap中事件的最小超时时间,计算I/O demultiplex的
5
**最大等待时间. 相反,如果有就绪事件,则清除tv,即I/O demultiplex不应该等待.
6
*/
7
if
(
!
base
->
event_count_active
&&
!
(flags
&
EVLOOP_NONBLOCK)) {
8
timeout_next(
base
,
&
tv_p);
9
}
else
{
10
/*
11
* if we have active events, we just poll new events
12
* without waiting.
13
*/
14
evutil_timerclear(
&
tv);
15
}
16
17
/*
If we have no events, we just exit
*/
18
//
没有事件处理,则退出循环
19
if
(
!
event_haveevents(
base
)) {
20
event_debug((
"
%s: no events registered.
"
, __func__));
21
return
(
1
);
22
}
23
24
//
tv_p为I/O demultiplex的超时时间
25
//
处理signal事件和I/O事件
26
res
=
evsel
->
dispatch(
base
, evbase, tv_p);
27
28
//
处理timeout事件,对于超时的事件,将其放到就绪事件链表
29
timeout_process(
base
);
30
31
if
(
base
->
event_count_active) {
32
//
处理就绪事件
33
event_process_active(
base
);
34
if
(
!
base
->
event_count_active
&&
(flags
&
EVLOOP_ONCE))
35
done
=
1
;
36
}
else
if
(flags
&
EVLOOP_NONBLOCK)
37
done
=
1
;
38
39
}
//
end while
40
3.3、timeout_next
1
/*
根据timer heap中事件的最小超时时间,计算I/O demultiplex的最大等待时间.
2
**为了及时处理timer事件,I/O demultiplex的最大等待时间不应该超过timer事件中最小的超时时间,
3
**否则,timer事件就不能得到及时处理
4
*/
5
static
int
6
timeout_next(
struct
event_base
*
base
,
struct
timeval
**
tv_p)
7
{
8
struct
timeval now;
9
struct
event
*
ev;
10
struct
timeval
*
tv
=
*
tv_p;
11
12
//
如果没有timer事件,则直接返回
13
if
((ev
=
min_heap_top(
&
base
->
timeheap))
==
NULL) {
14
/*
if no time-based events are active wait for I/O
*/
15
*
tv_p
=
NULL;
16
return
(
0
);
17
}
18
19
if
(gettime(
base
,
&
now)
==
-
1
)
20
return
(
-
1
);
21
22
//
如果最小的timer事件已经超时,则清除tv,即I/O demultiplex不应该等待.
23
if
(evutil_timercmp(
&
ev
->
ev_timeout,
&
now,
<=
)) {
24
evutil_timerclear(tv);
25
return
(
0
);
26
}
27
28
//
更新I/O demultiplex可以等待的最大时间:ev->ev_timeout - now
29
evutil_timersub(
&
ev
->
ev_timeout,
&
now, tv);
30
return
(
0
);
31
}
32
3.4、dispatch函数
调用底层I/O multiplex的dispatch函数,具体的实现可以参见epoll的实现epoll_dispatch。
3.5、event_process_active
代码
4、Timer事件
Timer事件的处理本身比较简单,不再赘述。
5、signal事件
5.1、socket pair
Libevent通过socketpair,将signal事件与I/O事件完美的统一起来。Socketpair,简单的说就一对socket,一端用于写,一端用于读。工作方式如下:
为了与I/O事件统一起来,libevent内部使用了一个针对read socket的读事件。
5.1.1、Socketpair的创建
与 信号事件的初始化工作都是在evsignal_init中完成的,而evsignal_init通过调用evutil_socketpair创建 socketpair。对于Unix平台,有socketpair系统调用;对于Windows,则相对复杂一些,具体见 evutil_socketpair函数的实现。
5.2、evsignal_info
在event_base内部有一个evsignal_info类型的字段sig,它是用于管理signal事件的核心数据结构:
代码
1
//
evsignal.h
2
struct
evsignal_info {
3
struct
event
ev_signal;
//
内部socket读事件
4
int
ev_signal_pair[
2
];
//
对应socket pair的两个socket描述符
5
int
ev_signal_added;
//
内部socket读事件是否已经加入注册链表
6
volatile
sig_atomic_t evsignal_caught;
//
是否有信号发生
7
//
信号事件链表数组,evsigevents[signo]表示注册信号signo的事件
8
struct
event_list evsigevents[NSIG];
9
//
具体记录每个信号触发的次数,evsigcaught[signo]是记录信号 signo被触发的次数
10
sig_atomic_t evsigcaught[NSIG];
11
12
//
sh_old记录了原来的 signal 处理函数指针,当信号 signo 注册的 event 被清空时,需要重新设置其处理函数
13
#ifdef HAVE_SIGACTION
14
struct
sigaction
**
sh_old;
15
#else
16
ev_sighandler_t
**
sh_old;
17
#endif
18
int
sh_old_max;
19
};
20
5.3、主要函数
5.3.1、evsignal_init
主要完成evsignal_info的初始化,主要算法:
代码
1
int
2
evsignal_init(
struct
event_base
*
base
)
3
{
4
evutil_socketpair(AF_UNIX, SOCK_STREAM,
0
,
base
->
sig.ev_signal_pair);
5
base
->
sig.sh_old
=
NULL;
6
base
->
sig.sh_old_max
=
0
;
7
8
//
事件发生次数设为0
9
base
->
sig.evsignal_caught
=
0
;
10
memset(
&
base
->
sig.evsigcaught,
0
,
sizeof
(sig_atomic_t)
*
NSIG);
11
/*
initialize the queues for all events
*/
12
for
(i
=
0
; i
<
NSIG;
++
i)
13
TAILQ_INIT(
&
base
->
sig.evsigevents[i]);
14
15
evutil_make_socket_nonblocking(
base
->
sig.ev_signal_pair[
0
]);
//
写端
16
17
//
设置内部读事件
18
event_set(
&
base
->
sig.ev_signal,
base
->
sig.ev_signal_pair[
1
],
19
EV_READ
|
EV_PERSIST, evsignal_cb,
&
base
->
sig.ev_signal);
//
读端
20
base
->
sig.ev_signal.ev_base
=
base
;
21
22
//
sig.ev_signal == EV_READ | EV_PERSIST | EVLIST_INTERNAL
23
base
->
sig.ev_signal.ev_flags
|=
EVLIST_INTERNAL;
24
}
该 函数的关键在于这里会设置libevent用于管理信号事件的内部读事件evsignal_info的ev_signal,并将该事件对应的文件描述符设 为socket pair的读端。该函数由I/O multiplex的init函数调用。注:这里只是设置,而并没有注册socket pair的读事件(见下一节)。
5.3.2、evsignal_add
当调用event_add注册信号事件时,内部会先调用 I/O multiplex的add函数,add函数又会调用evsignal_add,将事件加到evsignal_info内部的信号事件链表。然后再 event_queue_insert将其添加到event_base的注册事件链表。
代码
这里有两个地方需要注意,一是调用_evsignal_set_handler设置外部注册信号事件对应的信号的信号处理函数evsignal_handler:
代码
1
static
void
2
evsignal_handler(
int
sig)
3
{
4
int
save_errno
=
errno;
5
6
//
设置信号事件的发生次数
7
evsignal_base
->
sig.evsigcaught[sig]
++
;
8
evsignal_base
->
sig.evsignal_caught
=
1
;
9
10
#ifndef HAVE_SIGACTION
11
signal(sig, evsignal_handler);
12
#endif
13
14
/*
Wake up our notification mechanism
*/
15
//
向socket pair的写端写数据
16
send(evsignal_base
->
sig.ev_signal_pair[
0
],
"
a
"
,
1
,
0
);
17
errno
=
save_errno;
18
}
当用户注册信号事件对应的信号发生时,OS转到evsignal_handler函数,从而设置sig.evsignal_caught,并向socket pair的写端发送数据。
二是通过调用event_add完成内部socket pair的读事件sig->ev_signal的注册。最后,将(外部)事件添加到信号事件链表。
5.3.2、与主循环结合
信号事件完成了注册,libevent就会在主循环中,等待事件发生,并处理事件。为了理解,来看看具体I/O demultiplex的dispatch函数:
代码
1
static
int
2
epoll_dispatch(
struct
event_base
*
base
,
void
*
arg,
struct
timeval
*
tv)
3
{
4
struct
epollop
*
epollop
=
arg;
5
struct
epoll_event
*
events
=
epollop
->
events;
6
struct
evepoll
*
evep;
7
int
i, res, timeout
=
-
1
;
8
9
if
(tv
!=
NULL)
10
timeout
=
tv
->
tv_sec
*
1000
+
(tv
->
tv_usec
+
999
)
/
1000
;
11
12
if
(timeout
>
MAX_EPOLL_TIMEOUT_MSEC) {
13
/*
Linux kernels can wait forever if the timeout is too big;
14
* see comment on MAX_EPOLL_TIMEOUT_MSEC.
*/
15
timeout
=
MAX_EPOLL_TIMEOUT_MSEC;
16
}
17
18
//
等待I/O事件
19
res
=
epoll_wait(epollop
->
epfd, events, epollop
->
nevents, timeout);
20
21
if
(res
==
-
1
) {
22
if
(errno
!=
EINTR) {
23
event_warn(
"
epoll_wait
"
);
24
return
(
-
1
);
25
}
26
//
epoll_wait被信号中断
27
evsignal_process(
base
);
28
return
(
0
);
29
}
else
if
(
base
->
sig.evsignal_caught) {
//
发生了信号事件
30
//
处理信号事件
31
evsignal_process(
base
);
32
}
33
//
…
34
}
epoll_dispatch函数调用epoll_wait函数等待I/O发生。然后,如果有信号事件发生,则调用evsignal_process处理信号事件,evsignal_process的逻辑比较简单,它只是将事件从注册事件链表转移到就绪事件链表。
还 记得evsignal_handler函数吗?它是所有(外部)信号事件对应的信号的信号处理函数,将实际的信号发生时,OS会转而执行 evsignal_handler函数,而它便向socket pair的写端写数据,而读端收到数据。而此时,libevent的内部socket pair读事件已经完成注册。libevent正阻塞在epoll_wait处,当socketp pair读端收到数据时,libevent便从epoll_wait处返回。总之,signal事件通过socket pair,与I/O事件实现完美的统一。
Libevent从epoll_wait返回后,它调用evsignal_process处理信号事件,然后调用event_active将I/O事件(包括内部的socket pair读事件)转移到就绪事件链表。
Socket pair的读事件回调函数:
代码
1
static
void
2
evsignal_cb(
int
fd,
short
what,
void
*
arg)
3
{
4
static
char
signals[
1
];
5
#ifdef WIN32
6
SSIZE_T n;
7
#else
8
ssize_t n;
9
#endif
10
//
接收数据
11
n
=
recv(fd, signals,
sizeof
(signals),
0
);
12
if
(n
==
-
1
)
13
event_err(
1
,
"
%s: read
"
, __func__);
14
}
6、libevent的应用
Libevent 是一个非常优秀的开源网络库,它被许多其它开源程序使用。Memcache使用libevent作为底层的网络处理组件,并采用主线程(main thread,单一)+工作线程(work thread,多个)的多线程模型(这将在Memcached的分析中详细介绍)。