libevent源码浅析: 定时器和信号
http://godorz.info/2011/02/the-annotated-libevent-sources-about-timer-and-signal/
上一篇文章介绍了libevent下基本的I/O事件,这篇文章将讲讲libevent对定时器和信号事件的处理.
Timer事件
反应堆event_base包含了一个最小堆min_heap结构体的实例,以此维护注册到这个反应堆实例的定时器事件:
1.
struct
event_base {
2.
//其他成员
3.
struct
min_heap timeheap;
4.
};
回顾一下最小堆min_heap:
1.
typedef
struct
min_heap
2.
{
3.
//p指向一个动态分配的数组,数组元素是event指针.
4.
struct
event** p;
5.
unsigned n, a;
// n表示目前保存了多少元素,a表示p指向的内存能够存储event指针的个数
6.
} min_heap_t;
可以看到,它包含一个连续的内存块用于存储定时器事件.针对min_heap的操作主要有:
1.
static
inline
int
min_heap_push(min_heap_t* s,
struct
event* e);
2.
static
inline
struct
event* min_heap_pop(min_heap_t* s);
其中,min_heap_push()用于插入节点,min_heap_pop()用于弹出节点.其内部逻辑很简单,不必描述了.
现在看看libevent处理定时器事件的例子:
01.
static
void
timeout_cb(
int
fd,
short
event,
void
*arg) {...}
02.
03.
int
main (
int
argc,
char
**argv)
04.
{
05.
struct
event timeout;
06.
struct
timeval tv;
07.
08.
event_init();
09.
10.
evtimer_set(&timeout, timeout_cb, &timeout);
11.
12.
evutil_timerclear(&tv);
13.
tv.tv_sec = 2;
14.
event_add(&timeout, &tv);
15.
16.
lasttime =
time
(NULL);
17.
18.
event_dispatch();
19.
}
首先,和上篇文章例子一样的,event_init()初始化一个event_base(反应堆实例),然后由evtimer_set()设置定时器事件的回调函数,接着event_add()把定时器事件加入反应堆实例中.最后进入event_dispatch()主循环.
在这里,evtimer_set定义如下:
1.
#define evtimer_set(ev, cb, arg) event_set(ev, -1, 0, cb, arg)
至于event_set(),没有什么好说的,就是对一个event结构体做初始化罢了.
上一篇文章已经从I/O事件的角度介绍了event_add(),这里看看它是如何处理定时器事件的:
01.
int
event_add(
struct
event *ev,
const
struct
timeval *tv)
02.
{
03.
struct
event_base *base = ev->ev_base;
04.
05.
....
//处理IO事件或者信号事件的逻辑.
06.
07.
//如果tv不为0
08.
if
(tv != NULL)
09.
{
10.
event_queue_insert(base, ev, EVLIST_TIMEOUT);
11.
}
12.
}
可以看到,event_add()会把一个定时器事件压入到其对应的反应堆实例下的定时器最小堆timeheap中(&ev->base.timeheap).
回到event_dispatch(),它会调用event_base_loop(),此函数对定时器事件处理如下:
01.
//事件主循环
02.
int
03.
event_base_loop(
struct
event_base *base,
int
flags)
04.
{
05.
...
//不必多虑的其他代码
06.
07.
done = 0;
08.
while
(!done)
09.
{
10.
//检查heap中的timer events,将就绪的timer event从heap上删除,并插入到激活链表中,
11.
//这意味着base->event_count_active会增加
12.
timeout_process(base);
13.
14.
//有就绪事件了
15.
if
(base->event_count_active)
16.
{
17.
//处理就绪事件吧.
18.
event_process_active(base);
19.
}
20.
}
21.
}
其中,timeout_process()会将已超时的定时器事件插入到反应堆实例下的已就绪事件队列中,接着由event_process_active()处理已就绪事件.event_process_active()代码在上一篇文章中已经介绍过了,这里看一下timeout_process():
01.
/时间到~~~
02.
//开始处理base里面的定时器堆里的事件鸟.
03.
//检查heap中的timer events,将就绪的timer event从heap上删除,并插入到激活链表中
04.
void
timeout_process(
struct
event_base *base)
05.
{
06.
struct
timeval now;
07.
struct
event *ev;
08.
09.
while
((ev = min_heap_top(&base->timeheap)))
10.
{
11.
//ev超时时间的比现在的时间大,也就是说,这个ev还没有超时,那么while循环结束
12.
if
(evutil_timercmp(&ev->ev_timeout, &now, >))
13.
break
;
14.
15.
//else 意味着 evutil_timercmp(&ev->ev_timeout, &now, <=)为真
16.
//也就说明定时器最小堆的根超时了
17.
18.
//从定时器堆删除
19.
event_del(ev);
20.
21.
//把它插到激活链表吧.
22.
event_active(ev, EV_TIMEOUT, 1);
23.
}
24.
}
Signal事件
signal事件的处理时libevent中比较难懂的地方,前人之述不详,本文重点讲解之.
反应堆event_base包含了一个evsignal_info
结构体的实例,来维护注册到这个反应堆实例的信号事件:
1.
struct
event_base {
2.
//其他成员
3.
struct
evsignal_info sig;
4.
};
这里仔细研究一下evsignal_info结构体的定义:
01.
struct
evsignal_info {
02.
03.
//为 socket pair 的读 socket向 event_base 注册读事件时使用的 event 结构体
04.
//这个是所有信号事件共用的.
05.
struct
event ev_signal;
06.
07.
//这个也是所有信号事件共用的.
08.
int
ev_signal_pair[2];
09.
10.
//记录ev_signal 事件是否已经注册了
11.
int
ev_signal_added;
12.
13.
//是否有信号发生的标记
14.
//只在evsignal_handler()中被修改为1
15.
volatile
sig_atomic_t
sig_atomic_t
evsignal_caught;
16.
17.
//evsigevents[signo]表示注册到信号 signo 的事件链表
18.
struct
event_list evsigevents[NSIG];
19.
20.
//具体记录每个信号触发的次数,evsigcaught[signo]是记录信号signo被触发的次数
21.
sig_atomic_t
sig_atomic_t
evsigcaught[NSIG];
22.
23.
//记录了原来的signal处理函数指针,当信号signo注册的event被清空时,需要重新设置其处理函数
24.
struct
sigaction **sh_old;
25.
};
要了解evsignal_info为何是这样设计的,首先需要明白int ev_signal_pair[2];的作用.它实际上表示两个文件描述符,在libevent中一个用于写,一个用于读,它们在event_init()是被初始化.好吧,其实更确切点说,event_init()会调用event_base_new(),而event_base_new()调用封装好I/O多路复用技术的结构体eventop实例(&event_base->evsel)的init函数(&event_base->evsel),这个init函数会初始化eventop实例的内部数据结构,然后调用evsignal_init()对evsignal_info结构体实例(&event_base->sig)做初始化.而在初始化实例的过程中,对其内部的ev_signal_pair[2]数组的初始化是通过调用evutil_socketpair()函数来实现的.够了,上面这段话已经够恶心了,图示如下:
看看evutil_socketpair()代码:
1.
int
2.
evutil_socketpair(
int
family,
int
type,
int
protocol,
int
fd[2])
3.
{
4.
#ifndef WIN32
5.
return
socketpair(family, type, protocol, fd);
6.
#else
7.
...
//山寨一个socketpair函数
8.
}
它使用socketpair系统调用创建一对全双工管道(如果有时间的话,可以读一下evutil_socketpair()后半部分的代码,它在WIN32环境下如何山寨了一个socketpair函数,熟悉之可以加深不少理解.).这个全双工管道有什么用呢? 这里先卖个关子,我们看看evsignal_info结构体下的成员struct event ev_signal是如何被初始化的.
evsignal_init()调用event_set()函数,event_set()将&event_base->sig.ev_signal.ev_fd设置为&event_base->sig.ev_signal_pair[1],其回调函数为evsignal_cb(). ([1]).
至此,铺垫基本上做好了.我们看一个使用libevent处理信号事件的例子吧:
01.
static
void
signal_cb(
int
fd,
short
event,
void
*arg) {...}
02.
03.
int
main (
int
argc,
char
**argv)
04.
{
05.
/* Initalize the event library */
06.
event_init();
07.
08.
struct
event signal_int;
09.
event_set(&signal_int, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb, &signal_int);
10.
11.
event_add(&signal_int, NULL);
12.
13.
event_dispatch();
14.
}
首先是由event_init()创建一个反应堆实例(在此背后,对维护信号事件的结构体evsigal_info的实例(&event_base.sig)如何被初始化在上文已经做了介绍了.),然后由event_set()设置一个事件,将其标志&signal_int.events设为EV_SIGNAL|EV_PERSIST,文件描述符&signal_int.ev_fd设置对应的信号(在例子中是SIGIN,即中断信号,中断下可以用ctrl-c触发).然后设置好这个信号事件对应的回调函数 ([2]注意,回调函数对应的是信号事件,而非信号.注意与[3]的不同.).
之后,调用event_add()将信号事件注册到反应堆实例中,event_add()对信号事件的处理如下:
01.
int
event_add(
struct
event *ev,
const
struct
timeval *tv)
02.
{
03.
struct
event_base *base = ev->ev_base;
04.
const
struct
eventop *evsel = base->evsel;
05.
void
*evbase = base->evbase;
06.
int
res = 0;
07.
08.
//ev->ev_events表示事件类型
09.
//如果ev->ev_events是 读/写/信号 事件,而且ev不在 已注册链表 或 已激活链表,那么调用evbase注册ev事件
10.
if
((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
11.
!(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE)))
12.
{
13.
//实际执行操作的是evbase
14.
res = evsel->add(evbase, ev);
15.
16.
if
(res != -1)
//注册成功,把事件ev插入 已注册链表 中
17.
event_queue_insert(base, ev, EVLIST_INSERTED);
18.
}
19.
}
为了描述方便,我们假定libevent使用的I/O多路复用技术是select,看看select_add()代码吧:
1.
static
int
select_add(
void
*arg,
struct
event *ev)
2.
{
3.
if
(ev->ev_events & EV_SIGNAL)
4.
return
(evsignal_add(ev));
5.
}
对于信号事件,它转手给evsignal_add()函数处理,evsignal_add()代码如下:
01.
//将信号事件ev下的描述符ev_fd(也就是信号)添加到&ev->ev_base->sig->evsigevents[ev_fd]队列中
02.
int
evsignal_add(
struct
event *ev)
03.
{
04.
int
evsignal;
05.
struct
event_base *base = ev->ev_base;
06.
struct
evsignal_info *sig = &ev->ev_base->sig;
07.
08.
//拿到event下的信号标号
09.
evsignal = EVENT_SIGNAL(ev);
10.
11.
if
(TAILQ_EMPTY(&sig->evsigevents[evsignal]))
12.
{
13.
//设置这个事件对应的信号对应的处理函数
14.
15.
//watch out!!!!针对的是信号,不是事件
16.
if
(_evsignal_set_handler(
17.
base, evsignal, evsignal_handler) == -1)
18.
return
(-1);
19.
20.
//这里注册的sig本身,而不是信号事件
21.
//也就是就是说,sig是在真正有信号事件时才注册的.
22.
if
(!sig->ev_signal_added)
23.
{
24.
//注册这个信号对应的事件
25.
if
(event_add(&sig->ev_signal, NULL))
26.
return
(-1);
27.
sig->ev_signal_added = 1;
28.
}
29.
}
30.
31.
//多个事件可能对应同一信号
32.
TAILQ_INSERT_TAIL(&sig->evsigevents[evsignal], ev, ev_signal_next);
33.
}
evsignal_add()函数先获得信号事件对应的信号,通过_evsignal_set_handler()函数将此信号相应的信号处理函数设置为evsignal_handler(). ([3]注意,[2]设置的回调函数是针对信号事件的,这里设置的处理函数才是针对信号的.) 接着,evsignal_add()判断sig->ev_signal_added是否为0,为0则将&sig->ev_signal事件注册到反应堆实例中,然后将sig->ev_signal_added置1。;如果不为0,那么跳过这段代码.需要指出的是,sig->ev_signal_added唯一一次被置1就是在这段代码中,这保证了&sig->ev_signal事件只被注册到反应堆实例中一次.其实也就是说,只有在第1次有信号事件需要通过event_add()被注册到反应堆实例时,&sig->ev_signal事件才会被一起注册,这是libevent对&event_base->sig的延后处理.
<hr/>
接下来,貌似应该讲讲event_dispatch()对信号事件的处理了.且慢,我们回头把 [1], [2], [3] 整理一下:
(1) 在调用event_init()新建一个反应堆实例(以base表示)时,evsignal_info结构体(libevent用它来管理信号事件集合) base->sig被初始化,base->sig->ev_signal的回调函数总是被设置为evsignal_cb(),而evsignal_cb()是定义在libevent内部的,对libevent用户完全透明,其代码如下:
1.
static
void
2.
evsignal_cb(
int
fd,
short
what,
void
*arg)
3.
{
4.
recv(fd, signals,
sizeof
(signals), 0);
5.
}
它从一个文件描述符(后文会看到,这个文件描述符总是&event_base->sig.ev_signal_pair[1])读1比特的数据.
(2) 在已经通过调用event_init()获得一个反应堆实例后,通过event_set()设置一个信号事件signal_int的文件描述符signal_int.ev_fd(其实对于信号事件而言,ev_fd也就是此信号事件对应的信号),event_set()还设置了这个信号事件的回调函数.很明显,对于同一个信号,可以有不同的信号事件,这些信号事件的回调函数也可以完全不同.在这里,回调函数是由用户设计的,表示信号被触发时希望作出的反馈函数.
(3) 为了将一个事件(这个事件可以是I/O事件,也可以是定时器事件,也可以是信号事件)注册到反应堆实例中,我们必须调用event_add(),而event_add()通过重重调用,最终由evsignal_add()来完成将信号事件注册.回顾一下evsignal_add():
它通过_evsignal_set_handler总是将信号事件对应的信号的处理函数设置为evsignal_handler(),evsignal_handler()代码如下:
01.
//通知event_base有信号发生的技巧,往sig.ev_signal_pair[0]写1字节数据
02.
//会设置sig.evsignal_caught = 1,标记有信号产生.
03.
static
void
evsignal_handler(
int
sig)
04.
{
05.
evsignal_base->sig.evsigcaught[sig]++;
06.
evsignal_base->sig.evsignal_caught = 1;
//将信号发生标志至1
07.
08.
send(evsignal_base->sig.ev_signal_pair[0],
"a"
, 1, 0);
09.
}
它将信号发生标志evsignal_base->sig.evsignal_caught置1,以此通过libevent有信号发生.然后往&event_base->sig.ev_signal_pair[0]写1比特数据.
<hr/>
好吧,现在终于可以看看libevent是如何处理信号事件的了:
libevent先进入event_base_loop()主循环,等待已经准备好(可读可写或异常)的事件(通过select_dispatch找出已准备好的文件描述符).当有一个信号产生时,由于这个信号的信号处理函数(总是evsignal_handler())总是会往&event_base->sig.ev_signal_pair[0]写1比特数据(这是由操作系统调用的,对libevent是透明的,对libevent的用户就更加透明了).此时,根据前面的描述,由于ev_signal_pair[0]与ev_signal_pair[1]是一对全双工管道,所以,ev_signal_pair[1]将变得可读.而&event_base->sig.ev_signal事件的文件描述符正是ev_signal_pair[1],所以libevent可以知道&event_base->sig.ev_signal事件准备好了.为此,&event_base->sig.ev_signal事件被移入反应堆实例下的已就绪事件队列.接着在event_base_loop()的后续部分代码中被处理,通过event_process_active()调用其回调函数,也就是evsignal_cb(),从&event_base->sig.ev_signal_pair[1])读1比特的数据.[4]我们把信号被捕捉到的这个while()循环记为第1次while()循环.
写到这里,仍然有一个疑惑没有解开,上面都是讲libevent内部定义的&event_base->sig.ev_signal如何如何,可是我们希望的是自己定义的信号事件signal_int如何如何啊.
答案是,正如(3)描述的那样,在操作系统调用信号处理函数evsignal_handler()时,它会将信号发生标志置1.然后将evsignal_info结构体中用于记录信号被捕捉次数的evsigcaught[id]++,id也就是这个信号.
在第2此while()循环时(参考[4]),它还是调用select_dispatch(),这时,由于信号发生标志为1,所以select_dispatch()会调用函数evsignal_process().select_dispatch()相关代码如下:
1.
static
int
2.
select_dispatch(
struct
event_base *base,
void
*arg,
struct
timeval *tv)
3.
{
4.
if
(base->sig.evsignal_caught){
5.
evsignal_process(base);
6.
}
evsignal_process()代码如下:
01.
void
evsignal_process(
struct
event_base *base)
02.
{
03.
struct
evsignal_info *sig = &base->sig;
04.
struct
event *ev, *next_ev;
05.
sig_atomic_t
sig_atomic_t
ncalls;
06.
int
i;
07.
08.
base->sig.evsignal_caught = 0;
09.
for
(i = 1; i < NSIG; ++i)
10.
{
11.
ncalls = sig->evsigcaught[i];
12.
if
(ncalls == 0)
13.
continue
;
14.
sig->evsigcaught[i] -= ncalls;
15.
16.
for
(ev = TAILQ_FIRST(&sig->evsigevents[i]);
17.
ev != NULL; ev = next_ev)
18.
{
19.
next_ev = TAILQ_NEXT(ev, ev_signal_next);
20.
if
(!(ev->ev_events & EV_PERSIST))
21.
event_del(ev);
22.
23.
//移到已就绪事件队列,ncalls回调函数将会被调用多少次
24.
event_active(ev, EV_SIGNAL, ncalls);
25.
}
26.
}
27.
}
总结一下,反应堆结构体event_base有一个数据成员evsignal_info结构体,它维护信号事件集.之所以evsignal_info会有一个event事件成员ev_signal,是因为libevent通过socket pair让操作系统通知自己有信号发生,在信号处理函数中将信号发生标志置1,并使该信号被捕捉的次数自增,然后ev_signal被移到已就绪事件队列,接着被处理.然后libevent检查到信号发生标志已经被置1,遍历所有信号事件,找出信号被捕捉次数不为0的那个信号事件集,将它们移到已就绪事件队列,然后处理之.
以上,就是libevent处理信号事件的逻辑.