Libevent 是一个用C语言编写的、轻量级的开源高性能事件通知库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 *BSD 和 Mac Os;支持多种 I/O 多路复用技术, epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定时器和信号等事件;注册事件优先级。
实际上 Libevent只是对epoll、 poll、 dev/poll、 select 和 kqueue 等不同平台的io多路复用机制的简单的封装,但是在用libevent进行网络程序开发的时候感觉还是和直接用epoll、 poll进行快发有着根本的区别,所以通过袁源码分析一下libevent内部的运行原理,往下阅读前请先自行百度阅读epoll多路复用相关的信息。
首先看在我们进行创建socket,bind端口,listen之后开始事件循环调用的event.c函数event_base_loop,这里为了缩短篇幅会删除一些非重点的代码
int
event_base_loop(struct event_base *base, int flags)
{
const struct eventop *evsel = base->evsel;
struct timeval tv;
struct timeval *tv_p;
int res, done, retval = 0;
/* Grab the lock. We will release it inside evsel.dispatch, and again
* as we invoke user callbacks. */
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
........
while (!done) {
........
tv_p = &tv;
if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p); //计算最小的超时时长
} else {
/*
* if we have active events, we just poll new events
* without waiting.
*/
evutil_timerclear(&tv);
}
/* If we have no events, we just exit */
if (0==(flags&EVLOOP_NO_EXIT_ON_EMPTY) &&
!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
event_debug(("%s: no events registered.", __func__));
retval = 1;
goto done;
}
event_queue_make_later_events_active(base);
clear_time_cache(base);
分发事件
res = evsel->dispatch(base, tv_p);
/分发事件///
if (res == -1) {
event_debug(("%s: dispatch returned unsuccessfully.",
__func__));
retval = -1;
goto done;
}
update_time_cache(base);
//检测定时器超时事件//
timeout_process(base);
if (N_ACTIVE_CALLBACKS(base)) {
/处理激活的事件//
int n = event_process_active(base);
/处理激活的事件//
if ((flags & EVLOOP_ONCE)
&& N_ACTIVE_CALLBACKS(base) == 0
&& n != 0)
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
event_debug(("%s: asked to terminate loop.", __func__));
done:
clear_time_cache(base);
base->running_loop = 0;
EVBASE_RELEASE_LOCK(base, th_base_lock);
return (retval);
}
一 io事件的激活
libevent通过res = evsel->dispatch(base, tv_p)调用不同平台的多路复用函数,这里我们只看epoll,对应文件epoll.c,libevent通过eventtop结构把不同平台的io多路复用接口分装成统一的接口
const struct eventop epollops = {
"epoll",
epoll_init,
epoll_nochangelist_add,
epoll_nochangelist_del,
epoll_dispatch,
epoll_dealloc,
1, /* need reinit */
EV_FEATURE_ET|EV_FEATURE_O1|EV_FEATURE_EARLY_CLOSE,
0
};
接下来重点看epoll_dispatch,
static int
epoll_dispatch(struct event_base *base, struct timeval *tv)
{
struct epollop *epollop = base->evbase;
struct epoll_event *events = epollop->events;
int i, res;
long timeout = -1;
............
EVBASE_RELEASE_LOCK(base, th_base_lock);
//等待io事件返回
res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
if (res == -1) {
if (errno != EINTR) {
event_warn("epoll_wait");
return (-1);
}
return (0);
}
event_debug(("%s: epoll_wait reports %d", __func__, res));
EVUTIL_ASSERT(res <= epollop->nevents);
for (i = 0; i < res; i++) {
int what = events[i].events;
short ev = 0;
#ifdef USING_TIMERFD
if (events[i].data.fd == epollop->timerfd)
continue;
#endif
if (what & (EPOLLHUP|EPOLLERR)) {
ev = EV_READ | EV_WRITE;
} else {
if (what & EPOLLIN)
ev |= EV_READ;
if (what & EPOLLOUT)
ev |= EV_WRITE;
if (what & EPOLLRDHUP)
ev |= EV_CLOSED;
}
if (!ev)
continue;
evmap_io_active_(base, events[i].data.fd, ev | EV_ET);
}
//检测event内存池是否已满如果满了重新申请更大的内存空间
if (res == epollop->nevents && epollop->nevents < MAX_NEVENT) {
/* We used all of the event space this time. We should
be ready for more events next time. */
int new_nevents = epollop->nevents * 2;
struct epoll_event *new_events;
new_events = mm_realloc(epollop->events,
new_nevents * sizeof(struct epoll_event));
if (new_events) {
epollop->events = new_events;
epollop->nevents = new_nevents;
}
}
return (0);
}
在这里我们可以看到熟悉的epollwait调用,然后通过evmap_io_active_(base, events[i].data.fd, ev | EV_ET)来处理返回的io事件,准确的来说这里并没有立刻处理事件,而是插入到了一个激活事件队列的队列中。
event_base中有一个map。key是socket描述符fd,value是此socket关系的事件类型的集合,下面的GET_IO_SLOT(ctx, io, fd, evmap_io)即对此map的操作。
void
evmap_io_active_(struct event_base *base, evutil_socket_t fd, short events)
{
struct event_io_map *io = &base->io;
struct evmap_io *ctx;
struct event *ev;
#ifndef EVMAP_USE_HT
if (fd < 0 || fd >= io->nentries)
return;
#endif
GET_IO_SLOT(ctx, io, fd, evmap_io); //获取次fd关心的事件信息,map的find操作
if (NULL == ctx)
return;
LIST_FOREACH(ev, &ctx->events, ev_io_next) {
if (ev->ev_events & events) //如果fd注册了这个事件,即关注这个事件就插入激活队列
event_active_nolock_(ev, ev->ev_events & events, 1);
}
}
最终会调用event_active_nolock_插入到eventbase中的激活队列activequeues中注意这里还没有处理事件仅仅是插入队列中,详细的插入细节就不再叙述了。
struct event_base {
.....................
/** Mapping from file descriptors to enabled (added) events */
struct event_io_map io; //此fd上关心的事件
.....................
/** An array of nactivequeues queues for active event_callbacks (ones
* that have triggered, and whose callbacks need to be called). Low
* priority numbers are more important, and stall higher ones.
*/
struct evcallback_list *activequeues; //所有已经激活的事件队列
/** The length of the activequeues array *
int nactivequeues;//所有已经激活的事件数量
};
二 定时事件的激活
libevent如何把超时事件也注册到epoll中去呢,其实在之前的文章libevent如何管理event中已经提到过这个问题,就是利用epoll的超时来完成的,现在就从源码上分析一下。
首先我们还是看第一段代码,在调用evsel->dispatch(base, tv_p);,是传入tv_p就是epoll超时时间表示如果epoll_wait在传入的时间内仍然没有可读或者可写事件就立刻返回不在阻塞在这里,tv_p是通过timeout_next(base, &tv_p)计算得到的一个值。这里我们需要知道当你向event_base中注册一个超时事件时,libevent会把超时事件保存在一个小根堆中,堆的顶部就是最小的超时时间。
static int
timeout_next(struct event_base *base, struct timeval **tv_p)
{
/* Caller must hold th_base_lock */
struct timeval now;
struct event *ev;
struct timeval *tv = *tv_p;
int res = 0;
// 堆的首元素具有最小的超时值
ev = min_heap_top_(&base->timeheap);
// 如果没有定时事件,将等待时间设置为NULL,表示一直阻塞直到有I/O事件发生
if (ev == NULL) {
/* if no time-based events are active wait for I/O */
*tv_p = NULL;
goto out;
}
if (gettime(base, &now) == -1) {
res = -1;
goto out;
}
// 如果超时时间<=当前值,不能等待,需要立即返回
if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {
evutil_timerclear(tv);
goto out;
}
// 计算等待的时间=当前时间-最小的超时时间
evutil_timersub(&ev->ev_timeout, &now, tv);
EVUTIL_ASSERT(tv->tv_sec >= 0);
EVUTIL_ASSERT(tv->tv_usec >= 0);
event_debug(("timeout_next: event: %p, in %d seconds, %d useconds", ev, (int)tv->tv_sec, (int)tv->tv_usec));
out:
return (res);
}
接下来重新回到第一段代码,看第二段特殊注释的地方timeout_process,这里就是遍历小根堆中的超时事件直到超时时间大于当前时间跳出循环,并调用event_active_nolock_把所有的超时事件插入到之前提到过的activequeues中。注意这里仍然没有处理超时事件只是激活后插入到了队列中。
/* Activate every event whose timeout has elapsed. */
static void
timeout_process(struct event_base *base)
{
/* Caller must hold lock. */
struct timeval now;
struct event *ev;
if (min_heap_empty_(&base->timeheap)) {
return;
}
gettime(base, &now);
while ((ev = min_heap_top_(&base->timeheap))) {
if (evutil_timercmp(&ev->ev_timeout, &now, >))
break;
/* delete this event from the I/O queues */
event_del_nolock_(ev, EVENT_DEL_NOBLOCK);
event_debug(("timeout_process: event: %p, call %p",
ev, ev->ev_callback));
event_active_nolock_(ev, EV_TIMEOUT, 1);
}
}
三 事件的处理
仍然回到第一个代码块,事件的处理在第三个 特殊注释中,event_process_active(base),
static int
event_process_active(struct event_base *base)
{
/* Caller must hold th_base_lock */
struct evcallback_list *activeq = NULL;
int i, c = 0;
const struct timeval *endtime;
struct timeval tv;
const int maxcb = base->max_dispatch_callbacks;
const int limit_after_prio = base->limit_callbacks_after_prio;
if (base->max_dispatch_time.tv_sec >= 0) {
update_time_cache(base);
gettime(base, &tv);
evutil_timeradd(&base->max_dispatch_time, &tv, &tv);
endtime = &tv;
} else {
endtime = NULL;
}
for (i = 0; i < base->nactivequeues; ++i) {
if (TAILQ_FIRST(&base->activequeues[i]) != NULL) {
base->event_running_priority = i;
activeq = &base->activequeues[i];
if (i < limit_after_prio)
c = event_process_active_single_queue(base, activeq,
INT_MAX, NULL);
else
c = event_process_active_single_queue(base, activeq,
maxcb, endtime);
if (c < 0) {
goto done;
} else if (c > 0)
break; /* Processed a real event; do not
* consider lower-priority events */
/* If we get here, all of the events we processed
* were internal. Continue. */
}
}
done:
base->event_running_priority = -1;
return c;
}
这里会调用event_process_active_single_queue处理激活的事件,event_process_active_single_queue代码就不列出了,很简单做了一件事,就是从之前提到过的struct evcallback_list *activequeues的 evcallback_list 结构中得回调函数并执行。
在我们开发时通常是用bufferevent进行数据读写的,对于读写事件,这里的回调函数并不是我们编写程序时注册在bufferevent的回调,比如
#ifdef EVENT_THREAD_SAFE
m_pStBufEv = bufferevent_socket_new(GetReactor()->GetEventBase(),
m_oSocket.GetSocket(),
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE);
#else
m_pStBufEv = bufferevent_socket_new(GetReactor()->GetEventBase(),
m_oSocket.GetSocket(),
BEV_OPT_CLOSE_ON_FREE /*| BEV_OPT_THREADSAFE */);
#endif
MY_ASSERT_STR(NULL != m_pStBufEv, return false, "BufferEvent new failed!,error msg: %s", strerror(errno));
bufferevent_setcb(m_pStBufEv,
&IBufferEvent::lcb_OnRead,
&IBufferEvent::lcb_OnWrite,
&IBufferEvent::lcb_OnEvent,
(void *) this);
AfterBuffEventCreated();
// bufferevent_setwatermark(m_pStBufEv, EV_READ, 0, m_uMaxInBufferSize);
// bufferevent_setwatermark(m_pStBufEv, EV_WRITE, 0, m_uMaxOutBufferSize);
return true;
当有读写事件时并不是直接调用这里的 &IBufferEvent::lcb_OnRead, &IBufferEvent::lcb_OnWrite,而是libevent已经写好的读写回调。
struct bufferevent *
bufferevent_socket_new(struct event_base *base, evutil_socket_t fd,
int options)
{
struct bufferevent_private *bufev_p;
struct bufferevent *bufev;
#ifdef _WIN32
if (base && event_base_get_iocp_(base))
return bufferevent_async_new_(base, fd, options);
#endif
if ((bufev_p = mm_calloc(1, sizeof(struct bufferevent_private)))== NULL)
return NULL;
if (bufferevent_init_common_(bufev_p, base, &bufferevent_ops_socket,
options) < 0) {
mm_free(bufev_p);
return NULL;
}
bufev = &bufev_p->bev;
evbuffer_set_flags(bufev->output, EVBUFFER_FLAG_DRAINS_TO_FD);
///注册读写事件和回调///
event_assign(&bufev->ev_read, bufev->ev_base, fd,
EV_READ|EV_PERSIST|EV_FINALIZE, bufferevent_readcb, bufev);
event_assign(&bufev->ev_write, bufev->ev_base, fd,
EV_WRITE|EV_PERSIST|EV_FINALIZE, bufferevent_writecb, bufev);
///
evbuffer_add_cb(bufev->output, bufferevent_socket_outbuf_cb, bufev);
evbuffer_freeze(bufev->input, 0);
evbuffer_freeze(bufev->output, 1);
return bufev;
}
这里的bufferevent_readcb和bufferevent_writecb才是上面提到的回调。
static void
bufferevent_readcb(evutil_socket_t fd, short event, void *arg)
{
..........................................
evbuffer_unfreeze(input, 0);
res = evbuffer_read(input, fd, (int)howmuch); //读socket中的数据调用readv(fd, vecs, nvecs);
evbuffer_freeze(input, 0);
if (res == -1) {
int err = evutil_socket_geterror(fd);
if (EVUTIL_ERR_RW_RETRIABLE(err))
goto reschedule;
if (EVUTIL_ERR_CONNECT_REFUSED(err)) {
bufev_p->connection_refused = 1;
goto done;
}
/* error case */
what |= BEV_EVENT_ERROR;
} else if (res == 0) {
/* eof case */
what |= BEV_EVENT_EOF;
}
if (res <= 0)
goto error;
bufferevent_decrement_read_buckets_(bufev_p, res);
/* Invoke the user callback - must always be called last */
bufferevent_trigger_nolock_(bufev, EV_READ, 0); //调用回调函数
......................
}
static void
bufferevent_writecb(evutil_socket_t fd, short event, void *arg)
{
.......................
if (evbuffer_get_length(bufev->output)) { //如果bufferevent中的写缓冲区有待发送的数据则嗲用write发送数据
evbuffer_unfreeze(bufev->output, 1);
res = evbuffer_write_atmost(bufev->output, fd, atmost); // 调用write(fd, p, howmuch)发送数据
evbuffer_freeze(bufev->output, 1);
if (res == -1) {
int err = evutil_socket_geterror(fd);
if (EVUTIL_ERR_RW_RETRIABLE(err))
goto reschedule;
what |= BEV_EVENT_ERROR;
} else if (res == 0) {
/* eof case
XXXX Actually, a 0 on write doesn't indicate
an EOF. An ECONNRESET might be more typical.
*/
what |= BEV_EVENT_EOF;
}
if (res <= 0)
goto error;
bufferevent_decrement_write_buckets_(bufev_p, res);
}
if (evbuffer_get_length(bufev->output) == 0) {
event_del(&bufev->ev_write);
}
/*
* Invoke the user callback if our buffer is drained or below the
* low watermark.
*/
if (res || !connected) {
bufferevent_trigger_nolock_(bufev, EV_WRITE, 0); //调用回调函数
}
.....................
}
最终调用bufferevent_trigger_nolock_来回调我们自己注册的回调函数。
static inline void
bufferevent_trigger_nolock_(struct bufferevent *bufev, short iotype, int options)
{
if ((iotype & EV_READ) && ((options & BEV_TRIG_IGNORE_WATERMARKS) ||
evbuffer_get_length(bufev->input) >= bufev->wm_read.low))
bufferevent_run_readcb_(bufev, options);
if ((iotype & EV_WRITE) && ((options & BEV_TRIG_IGNORE_WATERMARKS) ||
evbuffer_get_length(bufev->output) <= bufev->wm_write.low))
bufferevent_run_writecb_(bufev, options);
}
在这里如果buffevent中读取操作使得输入缓冲区的数据量在此级别或者更高时,读取回调将被调用.默认值为0,所以每个读取操作都会导致读取回调被调用,写入操作使得输出缓冲区的数据量达到或者低于此级别时,写入回调将被调用.默认值是0,所以只有输出缓冲区空的时候才会调用写入回调,这里的回调就是我们注册的回调。
所以可以得到结论,当掉用读回调时其实libevent已经把可读的数据读到了buffevent中的读缓存,在我们调用buffevent的写函数发送数据时也只是把数据写到了buffevent中的写缓存中,等到epollwait下次返回对应的socket可写时才把缓存中的数据发送出去,并且如果我们开启BEV_OPT_THREADSAFE,libevent会对buffevent加锁,我们可以在多线程环境下操作buffevent读写数据。
这里要注意,libevent默认是水平触发的,写会一直返回可写即使我们没有数据要发送,因此如果我们需要通过bufferevent发送数据正确的操作应该是,默认情况不关心写事件,当有数据需要写时调用bufferevent_enable注册写事件然后把数据写buffervent写缓存,写完再会调用bufferevent_disable再卸载可写事件,避免没有数据要写时频繁的回调。
对于定时事件我们注册的回调就是epollwait返回后的第一层调用,libevent没有做任何处理,对于我们监听的sokcetlibevent调用event_assign(&lev->listener, base, fd, EV_READ|EV_PERSIST, listener_read_cb, lev);,当我们监听的socket可写时会在listener_read_cb中调用accept接收连接,然后才会回调我们的函数。
最后再说到多线程问题,在老的linux内核中如果我们在不同的线程同时监听一个socket时会有惊群效应,但是在新的linux内核中解决了这个问题,对于libevent如果我们创建eventbase前调用evthread_use_pthreads(),监听socket时调用evconnlistener_new_bind传入LEV_OPT_THREADSAFE,是可以在多线程中监听同一个socket的,但是究竟读写事件会在哪个线程中触发是随机的不可控的,并且从设计上很来说这种做法很乱没有章法,更严重的是其调度行为脱离了我们的掌控。
所以如果要在多线程使用libevent建议的做法也是最简单的做法就是单线程启动event_base事件循环,在此线程中负责监听,处理客户端连接,收发数据,然后分发消息到多线程中处理,缺点是在分发过程中就避免不了对消息队列加锁,有一定的锁的性能消耗。
另外一种复杂一点提高消息吞吐量的做法是,可以一个event_base线程专门负责listen fd处理新的连接,然后起一个线程池每个线程中都跑一个event_base。在listen fd线程中负给新的连接分配完socket后分配bufferevent时不要注册在自己的event_base中,而是选择性的注册在另外线程池中某个线程的event_base上,然后监听新的连接的读写事件处理其socket的读写逻辑,这样就不需要消息分发,每个线程负责一部分连接socket的读写并且处理其读写逻辑,避免多个线程监听同一个socket的同时并且控制了其触发逻辑,我们可以根据线程的繁忙程度适量的给每个线程分配新的连接任务。实时的调优系统负载,如下图。