安装与使用
libevent安装:下载地址:http://libevent.org/
- 解压文件:tar -zxvf libevent-2.1.8-stable.tar.gz
- 解压后进入目录,进行配置,把库安装到/usr目录下:./configure --prefix=/usr
- 编译安装:sudo make,sudo make install
libevent将I/O事件、信号事件、定时事件三种事件进行了同一事件源,将所有的就绪事件,放入到激活链表中;然后对激活链表中的事件,调用事件的回调函数执行事件处理
一、event_base
The event_base lies at the center of Libevent; every application will have one.对应的为Reactor实例,使用 libevent 函数之前需要分配一个或者多个 event_base 结构体。每个event_base
结构体持有一个事件集合,可以检测以 确定哪个事件是激活的(相当于epoll红黑树的树根)。
因为不是所有的安插在event_base的事件在调用fork()之后都可以正常工作,所以,如果在使用fork()或者其他相关系统调用启动一个新的进程之后,要想在子进程中使用base变量,但是又想让该base变量是一个全新的没有安插事件的变量,就应该在子进程中对base调用event_reinit函数进行重新初始化。
struct event_base { const struct eventop *evsel; void *evbase; int event_count; /* counts number of total events */ int event_count_active; /* counts number of active events */ int event_gotterm; /* Set to terminate loop */ int event_break; /* Set to terminate loop immediately */ /* active event management */ struct event_list **activequeues; int nactivequeues; /* signal handling info */ struct evsignal_info sig; struct event_list eventqueue; struct timeval event_tv; struct min_heap timeheap; struct timeval tv_cache; }; struct eventop { const char *name; void *(*init)(struct event_base *); // 初始化 int (*add)(void *, struct event *); // 注册事件 int (*del)(void *, struct event *); // 删除事件 19 int (*dispatch)(struct event_base *, void *, struct timeval *); // 事件分发 void (*dealloc)(struct event_base *, void *); // 注销,释放资源 /* set if we need to reinitialize the event base */ int need_reinit; };
- libevent将系统提供的I/O demultiplex机制统一封装成了eventop结构,因此evsel指向全局变量中的一个
- evbase实际上是一个eventop实例对象
- activequeues是一个二级指针,前面讲过libevent支持事件优先级,因此你可以把它看作是数组,其中的元素activequeues[priority]是一个链表,链表的每个节点指向一个优先级为priority的就绪事件event。
- eventqueue,链表,保存了所有的注册事件event的指针。
- sig是由来管理信号的结构体
- timeheap是管理定时事件的小根堆
- event_tv和tv_cache是libevent用于时间管理的变量
初始化和创建event_base
创建一个event_base对象也既是创建了一个新的libevent实例,程序需要通过调用event_init()(内部调用event_base_new函数执行具体操作)函数来创建,该函数同时还对新生成的libevent实例进行了初始化。
该函数首先为event_base实例申请空间,然后初始化timer mini-heap,选择并初始化合适的系统I/O 的demultiplexer机制,初始化各事件链表;
注册事件
向 Reactor(对应源码中的 event_base
) 中添加I/O、定时事件,需要调用 event_add
函数,信号事件调用 evsignal_add
,evsignal_add
最终也会调用 event_add
:
int event_add(struct event *ev, const struct timeval *tv); int event_add(struct event *ev, const struct timeval *tv) { struct event_base *base = ev->ev_base; // 要注册到的event_base const struct eventop *evsel = base->evsel; void *evbase = base->evbase; // base使用的系统I/O策略 // 新的timer事件,调用timer heap接口在堆上预留一个位置 // 注:这样能保证该操作的原子性: // 向系统I/O机制注册可能会失败,而当在堆上预留成功后, // 定时事件的添加将肯定不会失败; // 而预留位置的可能结果是堆扩充,但是内部元素并不会改变 if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) { if (min_heap_reserve(&base->timeheap,1 + min_heap_size(&base->timeheap)) == -1) return (-1); /* ENOMEM == errno */ } // 如果事件ev不在已注册或者激活链表中,则调用evbase注册事件 if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) && !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) { res = evsel->add(evbase, ev); if (res != -1) // 注册成功,插入event到已注册链表中 event_queue_insert(base, ev, EVLIST_INSERTED); } // 准备添加定时事件 if (res != -1 && tv != NULL) { struct timeval now; // EVLIST_TIMEOUT表明event已经在定时器堆中了,删除旧的 if (ev->ev_flags & EVLIST_TIMEOUT) event_queue_remove(base, ev, EVLIST_TIMEOUT); // 如果事件已经是就绪状态则从激活链表中删除 if ((ev->ev_flags & EVLIST_ACTIVE) &&(ev->ev_res & EV_TIMEOUT)) { // 将ev_callback调用次数设置为0 if (ev->ev_ncalls && ev->ev_pncalls) { *ev->ev_pncalls = 0; } event_queue_remove(base, ev, EVLIST_ACTIVE); } // 计算时间,并插入到timer小根堆中 gettime(base, &now); evutil_timeradd(&now, tv, &ev->ev_timeout); event_queue_insert(base, ev, EVLIST_TIMEOUT); } return (res); }
- ev:指向要注册的事件;
- tv:超时时间;
函数将ev注册到ev->ev_base上,事件类型由ev->ev_events指明,如果注册成功,ev将被插入到已注册链表中;如果tv不是NULL,则会同时注册定时事件,将ev添加到timer堆上;如果其中有一步操作失败,那么函数保证没有事件会被注册,可以讲这相当于一个原子操作。
event_queue_insert()负责将事件插入到对应的链表中
void event_queue_insert(struct event_base *base, struct event *ev, int queue) { // ev可能已经在激活列表中了,避免重复插入 if (ev->ev_flags & queue) { if (queue & EVLIST_ACTIVE) return; } // ... ev->ev_flags |= queue; // 记录queue标记 switch (queue) { case EVLIST_INSERTED: // I/O或Signal事件,加入已注册事件链表 TAILQ_INSERT_TAIL(&base->eventqueue, ev, ev_next); break; case EVLIST_ACTIVE: // 就绪事件,加入激活链表 base->event_count_active++; TAILQ_INSERT_TAIL(base->activequeues[ev->ev_pri], ev, ev_active_next); break; case EVLIST_TIMEOUT: // 定时事件,加入堆 min_heap_push(&base->timeheap, ev); break; } }
当事件激活(即所监听的事件发生),该事件会被插入激活队列,随后激活队列中的事件被处理(事件处理函数被调用),这部分工作在事件循环中完成:
事件循环的函数调用主要逻辑
- dispatch 函数调用 epoll_wait(只讨论使用了 epoll 的情况) 等待 I/O 事件发生
- epoll_wait 返回后,处理信号事件(epoll_wait 返回有可能是因为发生了信号事件,这和 Libevent 对信号事件的管理有关,详见信号事件管理)
- 将就绪的 I/O 事件添加到激活事件队列
- 将超时事件添加到激活事件队列
- event_process_active 统一处理激活事件队列中的事件(即调用每个事件对应的事件处理函数)
删除事件
int event_del(struct event *ev); int event_del(struct event *ev) { struct event_base *base; const struct eventop *evsel; void *evbase; // ev_base为NULL,表明ev没有被注册 if (ev->ev_base == NULL) return (-1); // 取得ev注册的event_base和eventop指针 base = ev->ev_base; evsel = base->evsel; evbase = base->evbase; // 将ev_callback调用次数设置为 if (ev->ev_ncalls && ev->ev_pncalls) { *ev->ev_pncalls = 0; } // 从对应的链表中删除 if (ev->ev_flags & EVLIST_TIMEOUT) event_queue_remove(base, ev, EVLIST_TIMEOUT); if (ev->ev_flags & EVLIST_ACTIVE) event_queue_remove(base, ev, EVLIST_ACTIVE); if (ev->ev_flags & EVLIST_INSERTED) { event_queue_remove(base, ev, EVLIST_INSERTED); // EVLIST_INSERTED表明是I/O或者Signal事件, // 需要调用I/O demultiplexer注销事件 return (evsel->del(evbase, ev)); } return (0); }
- 该函数将删除事件ev,对于I/O事件,从I/O 的demultiplexer上将事件注销;对于Signal事件,将从Signal事件链表中删除;对于定时事件,将从堆上删除;
- 同样删除事件的操作则不一定是原子的,比如删除时间事件之后,有可能从系统I/O机制中注销会失败。
二、event
libevent中的事件处理器是event结构类型。event结构体封装了句柄、事件类型、回调函数、以及其他必要的标志和数据.
struct event { TAILQ_ENTRY (event) ev_next; TAILQ_ENTRY (event) ev_active_next; TAILQ_ENTRY (event) ev_signal_next; unsigned int min_heap_idx; /* for managing timeouts */ struct event_base *ev_base; int ev_fd; short ev_events; short ev_ncalls; short *ev_pncalls; /* Allows deletes in callback */ struct timeval ev_timeout; int ev_pri; /* smaller numbers are higher priority */ void (*ev_callback)(int, short, void *arg); void *ev_arg; int ev_res; /* result passed to event callback */ int ev_flags; };
- ev_events:所支持的事件类型,可以用所示的标志按位与(互斥的标志不行,如读写事件和信号不能同时设置)
#define EV_TIMEOUT 0x01//定时事件 #define EV_READ 0x02//可读事件——I/O事件 #define EV_WRITE 0x04//可写事件——I/O事件 #define EV_SIGNAL 0x08//信号事件 #define EV_PERSIST 0x10//永久事件,事件被触发后自动重新对这个event调用event_add
- ev_next:libevent使用双向链表保存所有注册的I/O和Signal事件,ev_next就是该I/O事件在链表中的位置;称此链表为“已注册事件链表”;ev_signal_next就是signal事件在signal事件链表中的位置;
#define TAILQ_ENTIRY(type)\ struct\ { struct type *tqe_next;\ /*下一个元素*/ struct type **tqe_prev;\ /*前一个元素的地址*/ }
- ev_active_next:激活事件链表,libevent将所有的激活事件放入到链表active list中,然后遍历active list执行调度,ev_active_next就指明了event在active list中的位置;
- min_heap_idx和ev_timeout,如果是timeout事件,它们是event在小根堆中的索引和超时值,libevent使用小根堆来管理定时事件
- ev_base该事件所属的反应堆实例,这是一个event_base结构体
- ev_fd,对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号
- ev_callback,event的回调函数,被ev_base调用,执行事件处理程序,这是一个函数指针,原型为:
void (*ev_callback)(int fd, short events, void *arg); //fd:ev_fd //events:ev_events //arg:ev_arg,表明可以是任意类型的数据,在设置event时指定;
- _ev:所有相同描述符值的I/O事件处理器通过ev.ev_io.ev_io_next成员串成一个尾队列,称为I/O事件队列,所有相同信号值得事件处理器通过ev.ev_signal.ev_signal_next成员串成一个尾队列(信号事件队列),ev_ev_signal.ev_ncalls成员指定信号发生时,Reactor需要执行多少次该事该事件对应的件处理器中的回调函数,ev_ev_signal.ev_pncalls要么是NULL要么是指向ev_ev_signal.ev_ncalls.
- ev_ncalls:事件就绪执行时,调用ev_callback的次数,通常为1
- ev_pncalls:指针,通常指向ev_ncalls或者为NULL;
- ev_res:记录了当前激活事件的类型;
- 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已被初始化
每次当有事件event转变为就绪状态时,libevent就会把它移入到active event list[priority]中,其中priority是event的优先级;接着libevent会根据自己的调度策略选择就绪事件,调用其cb_callback()函数执行事件处理;并根据就绪的句柄和事件类型填充cb_callback函数的参数。
三、设置事件的接口
设置事件ev绑定的文件描述符或者信号,对于定时事件,设为-1即可
void event_set(struct event *ev, int fd, short events,void (*callback)(int, short, void *), void *arg);
- 设置事件类型,比如EV_READ|EV_PERSIST, EV_WRITE, EV_SIGNAL等;
- 设置事件的回调函数以及参数arg;
- 初始化其它字段,比如缺省的event_base和优先级;
设置event ev将要注册到的event_base
int event_base_set(struct event_base *base, struct event *ev);
libevent有一个全局event_base指针current_base,默认情况下事件ev将被注册到current_base上,使用该函数可以指定不同的event_base;如果一个进程中存在多个libevent实例,则必须要调用该函数为event设置不同的event_base;
设置event ev的优先级,没什么可说的,注意的一点就是:当ev正处于就绪状态时,不能设置,返回-1
int event_priority_set(struct event *ev, int pri);
四、统一事件源
I/O和timer事件统一
首先将Timer事件融合到系统I/O多路复用机制中,还是相当清晰的,因为系统的I/O机制像select()和epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有I/O事件发生,它们也保证能在timeout时间内返回。
那么根据所有Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活所有就绪的Timer事件就可以了,这样就能将Timer事件完美的融合到系统的I/O机制中了。
I/O和signal事件统一
Signal是异步事件的经典事例,将Signal事件统一到系统的I/O多路复用中就不像Timer事件那么自然了,Signal事件的出现对于进程来讲是完全随机的,进程不能只是测试一个变量来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行如下的操作”。
如果当Signal发生时,并不立即调用event的callback函数处理信号,而是设法通知系统的I/O机制,让其返回,然后再统一和I/O事件以及Timer一起处理,当signal事件发生时,可以使用pipe通知I/O多路复用机制。
五、集成信号处理事件
集成策略——使用socket pair
创建sockpair流程
集成到事件主循环——通知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()函数中,代码片段如下
res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout); if (res == -1) { if (errno != EINTR) { event_warn("epoll_wait"); return (-1); } evsignal_process(base);// 处理signal事件 return (0); } else if (base->sig.evsignal_caught) { evsignal_process(base);// 处理signal事件 }
- libevent中,初始化阶段并不注册读socket的读事件,而是在注册信号阶段才会测试并注册;
- libevent中,检查I/O事件是在各系统I/O机制的dispatch()函数中完成的,该dispatch()函数在event_base_loop()函数中被调用;
注册signal事件是通过evsignal_add(struct event *ev)函数完成的,libevent对所有的信号注册同一个处理函数evsignal_handler(),该函数将在下一段介绍,注册过程如下:
- 取得ev要注册到的信号signo;
- 如果信号signo未被注册,那么就为signo注册信号处理函数evsignal_handler();
- 如果事件ev_signal还没哟注册,就注册ev_signal事件;
- 将事件ev添加到signo的event链表中;
从signo上注销一个已注册的signal事件就更简单了,直接从其已注册事件的链表中移除即可。如果事件链表已空,那么就恢复旧的处理函数;
下面的讲解都以signal()函数为例,sigaction()函数的处理和signal()相似。处理函数evsignal_handler()函数做的事情很简单,就是记录信号的发生次数,并通知event_base有信号触发,需要处理:
static void evsignal_handler(int sig) { int save_errno = errno; // 不覆盖原来的错误代码 if (evsignal_base == NULL) { event_warn("%s: received signal %d, but have no base configured", __func__, sig); return; } // 记录信号sig的触发次数,并设置event触发标记 evsignal_base->sig.evsigcaught[sig]++; evsignal_base->sig.evsignal_caught = 1; #ifndef HAVE_SIGACTION signal(sig, evsignal_handler); // 重新注册信号 #endif // 向写socket写一个字节数据,触发event_base的I/O事件,从而通知其有信号触发,需要处理 send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0); errno = save_errno; // 错误代码 }
六、集成定时事件
因为系统的I/O机制像select()和epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有I/O事件发生,它们也保证能在timeout时间内返回。那么根据所有Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活所有就绪的Timer事件就可以了,这样就能将Timer事件完美的融合到系统的I/O机制中了。
具体的代码在源文件event.c的event_base_loop()中
if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) { // 根据Timer事件计算evsel->dispatch的最大等待时间 timeout_next(base, &tv_p); } else { // 如果还有活动事件,就不要等待,让evsel->dispatch立即返回 evutil_timerclear(&tv); } // ... // 调用select() or epoll_wait() 等待就绪I/O事件 res = evsel->dispatch(base, evbase, tv_p); // ... // 处理超时事件,将超时事件插入到激活链表中 timeout_process(base); //timeout_next()函数根据堆中具有最小超时值的事件和当前时间来计算等待时间 static int timeout_next(struct event_base *base, struct timeval **tv_p) { struct timeval now; struct event *ev; struct timeval *tv = *tv_p; // 堆的首元素具有最小的超时值 if ((ev = min_heap_top(&base->timeheap)) == NULL) { // 如果没有定时事件,将等待时间设置为NULL,表示一直阻塞直到有I/O事件发生 *tv_p = NULL; return (0); } // 取得当前时间 gettime(base, &now); // 如果超时时间<=当前值,不能等待,需要立即返回 if (evutil_timercmp(&ev->ev_timeout, &now, <=)) { evutil_timerclear(tv); return (0); } // 计算等待的时间=当前时间-最小的超时时间 evutil_timersub(&ev->ev_timeout, &now, tv); return (0); }
Timer小根堆
Libevent使用堆来管理Timer事件,其key值就是事件的超时时间
Heap[size++] <- new; // 先放到数组末尾,元素个数+1 // 下面就是shift_up()的代码逻辑,不断的将new向上调整 _child = size; while(_child>0) // 循环 { _parent (_child-1)/2; // 计算parent if(Heap[_parent].key < Heap[_child].key) break; // 调整结束,跳出循环 swap(_parent, _child); // 交换parent和child }
而libevent的heap代码对这一过程做了优化,在插入新元素时,只是为新元素预留了一个位置hole(初始时hole位于数组尾部),但并不立刻将新元素插入到hole上,而是不断向上调整hole的值,将父节点向下调整,最后确认hole就是新元素的所在位置时,才会真正的将新元素插入到hole上,因此在调整过程中就比上面的代码少了一次赋值的操作,代码逻辑是:
// 下面就是shift_up()的代码逻辑,不断的将new的“预留位置”向上调整 _hole = size; // _hole就是为new预留的位置,但并不立刻将new放上 while(_hole>0) // 循环 { _parent (_hole-1)/2; // 计算parent if(Heap[_parent].key < new.key) break; // 调整结束,跳出循环 Heap[_hole] = Heap[_parent]; // 将parent向下调整 _hole = _parent; // 将_hole调整到_parent } Heap[_hole] = new; // 调整结束,将new插入到_hole指示的位置 size++; // 元素个数+1
七、支持I/O多路复用
struct eventop { const char *name; void *(*init)(struct event_base *); // 初始化 int (*add)(void *, struct event *); // 注册事件 int (*del)(void *, struct event *); // 删除事件 int (*dispatch)(struct event_base *, void *, struct timeval *); // 事件分发 void (*dealloc)(struct event_base *, void *); // 注销,释放资源 /* set if we need to reinitialize the event base */ int need_reinit; };
Libevent把所有支持的I/O demultiplex机制存储在一个全局静态数组eventops中,并在初始化时选择使用何种机制或者可以自定义,数组内容根据优先级顺序声明如下:
/* In order of preference */ static const struct eventop *eventops[] = { #ifdef HAVE_EVENT_PORTS &evportops, #endif #ifdef HAVE_WORKING_KQUEUE &kqops, #endif #ifdef HAVE_EPOLL &epollops, #endif #ifdef HAVE_DEVPOLL &devpollops, #endif #ifdef HAVE_POLL &pollops, #endif #ifdef HAVE_SELECT &selectops, #endif #ifdef WIN32 &win32ops, #endif NULL };
然后libevent根据系统配置和编译选项决定使用哪一种I/O demultiplex机制,这段代码在函数event_base_new()中:
base->evbase = NULL; for (i = 0; eventops[i] && !base->evbase; i++) { base->evsel = eventops[i]; base->evbase = base->evsel->init(base); }
可以看出,libevent在编译阶段选择系统的I/O demultiplex机制,而不支持在运行阶段根据配置再次选择。
以Linux下面的epoll为例,实现在源文件epoll.c中,eventops对象epollops定义如下:
const struct eventop epollops = { "epoll", epoll_init, epoll_add, epoll_del, epoll_dispatch, epoll_dealloc, 1 /* need reinit */ };
变量epollops中的函数指针具体声明如下,注意到其返回值和参数都和eventop中的定义严格一致,这是函数指针的语法限制。
static void *epoll_init (struct event_base *); static int epoll_add (void *, struct event *); static int epoll_del (void *, struct event *); static int epoll_dispatch(struct event_base *, void *, struct timeval *); static void epoll_dealloc (struct event_base *, void *);
那么如果选择的是epoll,那么调用结构体eventop的init和dispatch函数指针时,实际调用的函数就是epoll的初始化函数epoll_init()和事件分发函数epoll_dispatch()了;
八、libevent支持多线程
错误示例
在多核的CPU上只使用一个线程始终是对不起CPU的处理能力啊,那好吧,那就多创建几个线程,比如下面的简单服务器场景。
- 主线程创建工作线程1;
- 接着主线程监听在端口上,等待新的连接;
- 在线程1中执行event事件循环,等待事件到来;
- 新连接到来,主线程调用libevent接口event_add将新连接注册到libevent上;
- … …
上面的逻辑看起来没什么错误,在很多服务器设计中都可能用到主线程和工作线程的模式….可是就在线程1注册事件时,主线程很可能也在操作事件,比如删除,修改,通过libevent的源代码也能看到,没有同步保护机制
消息通知+同步层
有个折中机制可以减少消息通信的开销,就是提取一个同步层,还拿上面的例子来说,你把工作安排都存放在一个工作队列中,而且你能够保证“任何人把新任务扔到这个队列”,“自己取出当前第一个任务”等这些操作都能够保证不会把队列搞乱(其实就是个加锁的队列容器)。
libevent官方文档:https://www.monkey.org/~provos/libevent/doxygen-2.0.1/include_2event2_2event_8h.html
来源:https://www.cnblogs.com/tianzeng/p/10799362.html