IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

libevent源码浅析: 定时器和信号

godorz... 2012-01-29 20:20:00 累计浏览 3,639 次
本机暂存

    上一篇文章介绍了libevent下基本的I/O事件,这篇文章将讲讲libevent对定时器和信号事件的处理.

Timer事件

    反应堆event_base包含了一个最小堆min_heap结构体的实例,以此维护注册到这个反应堆实例的定时器事件:

struct event_base {
        //其他成员
	struct min_heap timeheap;
};

    回顾一下最小堆min_heap:

typedef struct min_heap
{
    //p指向一个动态分配的数组,数组元素是event指针.
    struct event** p;
    unsigned n, a; // n表示目前保存了多少元素,a表示p指向的内存能够存储event指针的个数
} min_heap_t;

    可以看到,它包含一个连续的内存块用于存储定时器事件.针对min_heap的操作主要有:

static inline int min_heap_push(min_heap_t* s, struct event* e);
static inline struct event*  min_heap_pop(min_heap_t* s);

    其中,min_heap_push()用于插入节点,min_heap_pop()用于弹出节点.其内部逻辑很简单,不必描述了.

    现在看看libevent处理定时器事件的例子:

static void timeout_cb(int fd, short event, void *arg) {...}

int main (int argc, char **argv)
{
	struct event timeout;
	struct timeval tv;

        event_init();

	evtimer_set(&timeout, timeout_cb, &timeout);

	evutil_timerclear(&tv);
	tv.tv_sec = 2;
	event_add(&timeout, &tv);

	lasttime = time(NULL);

	event_dispatch();
}

    首先,和上篇文章例子一样的,event_init()初始化一个event_base(反应堆实例),然后由evtimer_set()设置定时器事件的回调函数,接着event_add()把定时器事件加入反应堆实例中.最后进入event_dispatch()主循环.

    在这里,evtimer_set定义如下:

#define evtimer_set(ev, cb, arg)	event_set(ev, -1, 0, cb, arg)

    至于event_set(),没有什么好说的,就是对一个event结构体做初始化罢了.

    上一篇文章已经从I/O事件的角度介绍了event_add(),这里看看它是如何处理定时器事件的:

int event_add(struct event *ev, const struct timeval *tv)
{
    struct event_base *base = ev->ev_base;

    ....//处理IO事件或者信号事件的逻辑.

    //如果tv不为0
    if (tv != NULL)
    {
        event_queue_insert(base, ev, EVLIST_TIMEOUT);
    }
}

    可以看到,event_add()会把一个定时器事件压入到其对应的反应堆实例下的定时器最小堆timeheap中(&ev->base.timeheap).

    回到event_dispatch(),它会调用event_base_loop(),此函数对定时器事件处理如下:

//事件主循环
int
event_base_loop(struct event_base *base, int flags)
{
    ...//不必多虑的其他代码

    done = 0;
    while (!done)
    {
	//检查heap中的timer events,将就绪的timer event从heap上删除,并插入到激活链表中,
	//这意味着base->event_count_active会增加
        timeout_process(base);

	//有就绪事件了
        if (base->event_count_active)
        {
            //处理就绪事件吧.
            event_process_active(base);
        }
    }
}

    其中,timeout_process()会将已超时的定时器事件插入到反应堆实例下的已就绪事件队列中,接着由event_process_active()处理已就绪事件.event_process_active()代码在上一篇文章中已经介绍过了,这里看一下timeout_process():

/时间到~~~
//开始处理base里面的定时器堆里的事件鸟.
//检查heap中的timer events,将就绪的timer event从heap上删除,并插入到激活链表中
void timeout_process(struct event_base *base)
{
    struct timeval now;
    struct event *ev;

   while ((ev = min_heap_top(&base->timeheap)))
    {
    	//ev超时时间的比现在的时间大,也就是说,这个ev还没有超时,那么while循环结束
        if (evutil_timercmp(&ev->ev_timeout, &now, >))
            break;

	//else 意味着 evutil_timercmp(&ev->ev_timeout, &now, <=)为真
	//也就说明定时器最小堆的根超时了

        //从定时器堆删除
        event_del(ev);

	//把它插到激活链表吧.
        event_active(ev, EV_TIMEOUT, 1);
    }
}

Signal事件

    signal事件的处理时libevent中比较难懂的地方,前人之述不详,本文重点讲解之.

    反应堆event_base包含了一个evsignal_info结构体的实例,来维护注册到这个反应堆实例的信号事件:

struct event_base {
        //其他成员
	struct evsignal_info sig;
};

    这里仔细研究一下evsignal_info结构体的定义:

struct evsignal_info {

	//为 socket pair 的读 socket向 event_base 注册读事件时使用的 event 结构体
	//这个是所有信号事件共用的.
	struct event ev_signal; 

	//这个也是所有信号事件共用的.
	int ev_signal_pair[2]; 

	//记录ev_signal 事件是否已经注册了
	int ev_signal_added;

	//是否有信号发生的标记
	//只在evsignal_handler()中被修改为1
	volatile sig_atomic_t evsignal_caught;

	//evsigevents[signo]表示注册到信号 signo 的事件链表
	struct event_list evsigevents[NSIG];

	//具体记录每个信号触发的次数,evsigcaught[signo]是记录信号signo被触发的次数
	sig_atomic_t evsigcaught[NSIG];

	//记录了原来的signal处理函数指针,当信号signo注册的event被清空时,需要重新设置其处理函数
	struct sigaction **sh_old;
};

    要了解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()代码:

int
evutil_socketpair(int family, int type, int protocol, int fd[2])
{
#ifndef WIN32
	return socketpair(family, type, protocol, fd);
#else
       ...//山寨一个socketpair函数
}

    它使用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处理信号事件的例子吧:

static void signal_cb(int fd, short event, void *arg) {...}

int main (int argc, char **argv)
{
	/* Initalize the event library */
        event_init();

	struct event signal_int;
	event_set(&signal_int, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb, &signal_int);

	event_add(&signal_int, NULL);

	event_dispatch();
}

    首先是由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()对信号事件的处理如下:

int event_add(struct event *ev, const struct timeval *tv)
{
    struct event_base *base = ev->ev_base;
    const struct eventop *evsel = base->evsel;
    void *evbase = base->evbase;
    int res = 0;

    //ev->ev_events表示事件类型
    //如果ev->ev_events是 读/写/信号 事件,而且ev不在 已注册链表 或 已激活链表,那么调用evbase注册ev事件
    if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
            !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE)))
    {
	//实际执行操作的是evbase
        res = evsel->add(evbase, ev);

        if (res != -1) //注册成功,把事件ev插入 已注册链表 中
            event_queue_insert(base, ev, EVLIST_INSERTED);
    }
}

    为了描述方便,我们假定libevent使用的I/O多路复用技术是select,看看select_add()代码吧:

static int select_add(void *arg, struct event *ev)
{
	if (ev->ev_events & EV_SIGNAL)
		return (evsignal_add(ev));
}

    对于信号事件,它转手给evsignal_add()函数处理,evsignal_add()代码如下:

//将信号事件ev下的描述符ev_fd(也就是信号)添加到&ev->ev_base->sig->evsigevents[ev_fd]队列中
int evsignal_add(struct event *ev)
{
    int evsignal;
    struct event_base *base = ev->ev_base;
    struct evsignal_info *sig = &ev->ev_base->sig;

    //拿到event下的信号标号
    evsignal = EVENT_SIGNAL(ev);

    if (TAILQ_EMPTY(&sig->evsigevents[evsignal]))
    {
		//设置这个事件对应的信号对应的处理函数

		//watch out!!!!针对的是信号,不是事件
	if (_evsignal_set_handler(
                    base, evsignal, evsignal_handler) == -1)
            return (-1);

	//这里注册的sig本身,而不是信号事件
	//也就是就是说,sig是在真正有信号事件时才注册的.
        if (!sig->ev_signal_added)
        {
            //注册这个信号对应的事件
            if (event_add(&sig->ev_signal, NULL))
                return (-1);
            sig->ev_signal_added = 1;
        }
    }

    //多个事件可能对应同一信号
    TAILQ_INSERT_TAIL(&sig->evsigevents[evsignal], ev, ev_signal_next);
}

    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的延后处理.


    接下来,貌似应该讲讲event_dispatch()对信号事件的处理了.且慢,我们回头把 [1], [2], [3] 整理一下:

    (1) 在调用event_init()新建一个反应堆实例(以base表示)时,evsignal_info结构体(libevent用它来管理信号事件集合) base->sig被初始化,base->sig->ev_signal的回调函数总是被设置为evsignal_cb(),而evsignal_cb()是定义在libevent内部的,对libevent用户完全透明,其代码如下:

static void
evsignal_cb(int fd, short what, void *arg)
{
    recv(fd, signals, sizeof(signals), 0);
}

    它从一个文件描述符(后文会看到,这个文件描述符总是&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()代码如下:

//通知event_base有信号发生的技巧,往sig.ev_signal_pair[0]写1字节数据
//会设置sig.evsignal_caught = 1,标记有信号产生.
static void evsignal_handler(int sig)
{
    evsignal_base->sig.evsigcaught[sig]++;
    evsignal_base->sig.evsignal_caught = 1; //将信号发生标志至1

    send(evsignal_base->sig.ev_signal_pair[0], \"a\", 1, 0);
}

    它将信号发生标志evsignal_base->sig.evsignal_caught置1,以此通过libevent有信号发生.然后往&event_base->sig.ev_signal_pair[0]写1比特数据.


    好吧,现在终于可以看看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()相关代码如下:

static int
select_dispatch(struct event_base *base, void *arg, struct timeval *tv)
{
        if (base->sig.evsignal_caught){
		evsignal_process(base);
}

    evsignal_process()代码如下:

void evsignal_process(struct event_base *base)
{
    struct evsignal_info *sig = &base->sig;
    struct event *ev, *next_ev;
    sig_atomic_t ncalls;
    int i;

    base->sig.evsignal_caught = 0;
    for (i = 1; i < NSIG; ++i)
    {
        ncalls = sig->evsigcaught[i];
        if (ncalls == 0)
            continue;
        sig->evsigcaught[i] -= ncalls;

        for (ev = TAILQ_FIRST(&sig->evsigevents[i]);
                ev != NULL; ev = next_ev)
        {
            next_ev = TAILQ_NEXT(ev, ev_signal_next);
            if (!(ev->ev_events & EV_PERSIST))
                event_del(ev);

	    //移到已就绪事件队列,ncalls回调函数将会被调用多少次
            event_active(ev, EV_SIGNAL, ncalls);
        }
    }
}

    总结一下,反应堆结构体event_base有一个数据成员evsignal_info结构体,它维护信号事件集.之所以evsignal_info会有一个event事件成员ev_signal,是因为libevent通过socket pair让操作系统通知自己有信号发生,在信号处理函数中将信号发生标志置1,并使该信号被捕捉的次数自增,然后ev_signal被移到已就绪事件队列,接着被处理.然后libevent检查到信号发生标志已经被置1,遍历所有信号事件,找出信号被捕捉次数不为0的那个信号事件集,将它们移到已就绪事件队列,然后处理之.

    以上,就是libevent处理信号事件的逻辑.

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. memcached 源码阅读笔记 (累计阅读 5,402)
  2. 学习libevent的select模型 (累计阅读 5,094)
  3. bash下利用trap捕捉信号量 (累计阅读 4,926)
  4. php实现的thrift socket server (累计阅读 4,852)
  5. Memcache源代码分析之网络处理 (累计阅读 4,647)
  6. Memcached的线程模型及状态机 (累计阅读 4,496)
  7. Redis的事件循环与定时器模型 (累计阅读 4,047)
  8. php的异步http请求类 (累计阅读 3,829)
  9. 关于php的libevent扩展的应用 (累计阅读 3,808)
  10. Chaos网络库(二)- Buffer的设计 (累计阅读 2,234)