libevent 网络IO分析

1 简介

libevent 是一个基于事件触发的网络库,轻量级,代码精炼易读且跨平台,其底层会根据所运行的平台选择对应的 I/O 复用机制,libevent 是一个典型的 reactor 设计模式,简单的说加入你对某一事件感兴趣,比如想知道某个 socket 是否可读,你可以把这一事件与某个处理该事件的回调函数关联起来形成一个 event,然后注册到 libvent 内核,当该事件发生的时候 libvent 就会通过该 event 调用你给的回调函数。 写这一文章的目的主要是为了总结自己在学习与阅读 libevent 源代码过程中的经验与知识,也可供以后参考,本文主要是基于 libvent1.4.15 进行分析。

2 简单使用与入门

首先来看几个简单的使用例子程序,通过这些代码我们可以快速的入门,了解 libevent 的大致用法

2.1 定时器-timeout 超时回调

下面的代码实现了一个定时调用回调函数 timeout_cb 的功能,详细请看代码注释:

int lasttime;

static void
timeout_cb(int fd /*超时回调,没有用*/,
short event /*libevent 调用该回调时告知用户发生的事件,此处应该是 EV_TIMEOUT*/,
void *arg /*注册的时候设置的参数*/)
{
struct timeval tv;
struct event *timeout = arg;
int newtime = time(NULL); printf("%s: called at %d: %d\n", __func__, newtime,
newtime - lasttime);
lasttime = newtime; evutil_timerclear(&tv);
tv.tv_sec = 2;
//调用一次之后再注册该事件,2s 之后通知我
//如果不添加,libevent 中就没有 event 会自动退出
event_add(timeout, &tv);
} int
main (int argc, char **argv)
{
//libevent 的一个 event,用于关联 handle 与 callback
struct event timeout;
struct timeval tv; /* Initalize the event library 初始化*/
event_init(); /* Initalize one event */
evtimer_set(&timeout/*设置 event*/,
timeout_cb /*回调函数,看原型*/,
&timeout/*调用回调函数时传给回调函数的参数 arg*/); evutil_timerclear(&tv); // 初始化事件结构体
tv.tv_sec = 2;
event_add(&timeout, &tv); //注册 event,设置超时时间为 2 秒调用一次 lasttime = time(NULL); event_dispatch();//运行 libevent,进行事件分发,这里会阻塞 return (0);
}

2.2 信号事件

当运行一下程序时,中断该程序 3 次就会退出程序

int called = 0;
static void
signal_cb(int fd, short event, void *arg)
{ struct event *signal = arg; printf("%s: got signal %d\n", __func__, EVENT_SIGNAL(signal)); if (called >= 2)//第三次调用就会将该 event del 注销掉
event_del(signal); called++;
}
int
main (int argc, char **argv)
{
#ifdef WIN32
{
//win32 下要初始化 winsock2,否则运行不成功,官方的 sample 代码在 win32 运行不成功
WORD winsock_ver = MAKEWORD(2, 2);
WSAData wsa_data;
bool did_init_ = (WSAStartup(winsock_ver, &wsa_data) == 0); if (did_init_)
{
assert(wsa_data.wVersion == winsock_ver);
WSAGetLastError();
}
}
#endif struct event signal_int; /*
* 新建一个 libevent 实例,实际上例子 timeout 中的 event_init 仅仅是对 event_base_new 的封装
* 然后赋值给一个全局变量 event_base *current_base
*/
struct event_base* base = event_base_new(); /* Initalize one event */
/*标识 EV_PERSIST 表示永久事件,添加一次即可,回调 callback 后 libevent 会自动重新注册
*不用用户自动添加,例子 1 的 timeout 非 EV_PERSIST 事件,需要自己添加*/
event_set(&signal_int, SIGINT, EV_SIGNAL|EV_PERSIST,
signal_cb,
&signal_int);
event_base_set(base, &signal_int); //将该 event 与新建的 libevent 实例关联 event_add(&signal_int, NULL); //添加到 base 内核中 event_base_dispatch(base);
event_base_free(base); //最后要释放我们新建的实例 return (0);
}

2.3 读取 socket

该例子通过创建一个 socket pair 然后往其中写入数据,然后将另外一个 socket 注册到 libvent 中,当该 socket 可能,我们的回调函数就会被调用,(代码从官方的测试代码中截取,未运行是否通过)

int pair[2];
int test_ok;
static void
simple_read_cb(int fd, short event, void *arg)
{
char buf[256];
int len; if (arg == NULL)
return; len = read(fd, buf, sizeof(buf)); if (len) {
if (!called) {
if (event_add(arg, NULL) == -1)
exit(1);
}
} else if (called == 1)
test_ok = 1; called++;
}
int main(void)
{
#ifdef WIN32
WORD wVersionRequested;
WSADATA wsaData;
int err; wVersionRequested = MAKEWORD( 2, 2 ); err = WSAStartup( wVersionRequested, &wsaData );
#endif
struct event_base *base;
struct event ev1;
/*创建 socket pair*/
if (evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == -1) {
fprintf(stderr, "%s: socketpair\n", __func__);
exit(1);
}
/*写入数据之后关闭*/
write(pair[0], TEST1, strlen(TEST1)+1);
shutdown(pair[0], SHUT_WR); base = event_base_new();
/*监视该 socket 是否可读,可读的话回调我们的 callback*/
event_set(&ev1, pair[1], EV_READ, simple_read_cb, &ev1);
event_base_set(base, &ev1);
event_add(&ev1, NULL);
test_ok = 0;
event_base_dispatch(base); event_base_free(base);
}

3 操作系统 I/O 模型封装

(简绍一款 windows 下分析源代码神器 source insight,linux 下可用 wine 运行) libevent 将各个平台的 I/O 模型封装抽象化,然后通过函数指针的方式进行调用,上层代码只关注 event 的处理,底层与句柄相关如 socket,file,singnal,timeout 相关的部分交给操作系统进行处理,libevent 上层代码通过结构体 struct eventop 中的几个函数指针 init、add、del、dispatch 和 dealloc 调用与操作系统相关的代码,eventop 相当于一个抽象类,其中的几个函数指针相当于纯虚函数,不同的 i/o 模型则是该 eventop 的子类。struct eventop 原型如下:

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;
};

与特定平台相关的 I/O 多路复用模块都有一个全局的 struct eventop 变量如:

  • socket 编程中的 select, 源文件 select.c: const struct eventop selectops
  • win32 平台,源文件 win32.c: struct eventop win32ops (实际是使用 select,由于 win32 下的 select 的关系导致 libevent 在 win32 下性能不如意)

最后在 event.c 中将这几个与操作系统底层相关的模块汇总,根据特定平台或者定义的宏选择合适的 I/O 模型进行编译:

/* 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
};

4 源码分析-基本功能

 

4.1 超时机制-对 timeout 例子进行源码分析

libvent 提供了超时机制,比如注册某个 event,希望过了某个特定的时间 timeout(超时)后调用我们的 callback(回调函数),实现该功能主要用到的数据结构是二叉堆 binary-heap(以前是红黑树 rb-tree),超时功能可以实现

  • 定时器检测某个进程是否正常运行或者文件是否已被更新
  • 游戏编程里面画面的绘制及帧控制
  • 网络编程中的心跳包发送

4.1.1 libevent 初始化

event_init 初始化 libevent,该函数其实是创建一个 libvent 实例将其赋值给一个全局变量参看以下代码,我们可以通过 event_base_new 自己新建一个实例然后丢到某个线程 dispatch

struct event_base *
event_init(void)
{
struct event_base *base = event_base_new(); if (base != NULL)
current_base = base; //current_base 实际是一个全局变量 return (base);
}

struct event_base 是 libvent 的核心结构体,后面的代码基本是与 event_base 打交道,原型如下:

struct event_base {
const struct eventop *evsel; // 选择的 i/o 复用模型
void *evbase; //调用 i/o 模型 evsel->init 返回的变量,之后调用与 evsel 相关的函数都会将该变量传入
int event_count; /* counts number of total events 当前注册的 event 总数*/
int event_count_active; /* counts number of active events 处于活动队列的 event 总数,即即将被回调的 event*/
int event_gotterm; /* Set to terminate loop 正常退出 dispatch*/
int event_break; /* Set to terminate loop immediately 马上退出 dispatch*/
/* active event management */
//1. active list 即将被回调
// - 比如注册一个 2s timeout event,2s 过后该 event 会被放到该 list 等待被回调
// - 注册一个 socket read event,当 socket 可读会将与该 socket 关联的 event 放到 list 等待回调
//2. 指针数组的原因是要实现一个优先级,数组头优先级最高,先被调用
struct event_list **activequeues;
int nactivequeues; //activequeues 数组元素个数
/* signal handling info */
struct evsignal_info sig; //信号相关
struct event_list eventqueue; //插入的所有 event
struct timeval event_tv;
struct min_heap timeheap; //二叉堆
struct timeval tv_cache;
};

实际创建于初始化的过程在 event_base_new 中进行,可以看到初始化操作系统相关的 io 模型过程是遍历 eventops 数组调用其元素 eventtops[i]->init 后赋值给 base->evbase

struct event_base *
event_base_new(void)
{
int i;
struct event_base *base; if ((base = calloc(1, sizeof(struct event_base))) == NULL)
event_err(1, "%s: calloc", __func__); event_sigcb = NULL;
event_gotsig = 0; detect_monotonic();
gettime(base, &base->event_tv); min_heap_ctor(&base->timeheap);
TAILQ_INIT(&base->eventqueue);
base->sig.ev_signal_pair[0] = -1;
base->sig.ev_signal_pair[1] = -1; base->evbase = NULL;
//调用平台相关的初始化过程,并复制给 base->evbase
for (i = 0; eventops[i] && !base->evbase; i++) {
base->evsel = eventops[i];
base->evbase = base->evsel->init(base);
} if (base->evbase == NULL)
event_errx(1, "%s: no event mechanism available", __func__); if (evutil_getenv("EVENT_SHOW_METHOD"))
event_msgx("libevent using: %s\n",
base->evsel->name); /* allocate a single active event queue */
event_base_priority_init(base, 1); //优先级队列,后面讲解 return (base);
}

4.1.2 注册 event

在 timeout 例子中首先是调用 evtimer_set 初始化 event 后在 event_add 到 libevent 实例,evtimer_set 只是一个宏,其实际调用的是 event_set:

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

struct event 能够与我们的 handle 关联然后注册到 libvent 中,其中的原型定义以及注释如下,一些字段开始不理解没关系,可以往下阅读然后回来参考

//event 中有三个链表节点,用于插入到 event_base 和 singnal_info.list 中
//该链表实现可以参看 queue.h,实现得非常精巧,对于理解后续代码很有帮助
struct event {
TAILQ_ENTRY (event) ev_next; //所有已注册的 event
TAILQ_ENTRY (event) ev_active_next; //active list
TAILQ_ENTRY (event) ev_signal_next; //singnal list
unsigned int min_heap_idx; /* for managing timeouts 最小堆,标识自己在堆中的位置,主要给对函数操作*/ struct event_base *ev_base; //指向 dispatch 自己的 event_base int ev_fd; //关联的文件描述符,timeout event 被忽略
short ev_events; //监听的事件
short ev_ncalls; //插入 active 之后要被调用的次数
short *ev_pncalls; /* Allows deletes in callback 通过该变量可以再调用过程中删除,不用关心*/ //超时的时间与 min_heap_idx 配合使用,用于二叉堆排序,时间最小最快发生的在堆顶
struct timeval ev_timeout;
/* 优先级,将被回调时字段 ev_active_next 插在 ev_base->activequeues[ev_pri]中*/
int ev_pri; //指定的回调函数与参数
void (*ev_callback)(int, short, void *arg);
void *ev_arg; int ev_res; /* result passed to event callback */
int ev_flags;////标志位,标志该 event 在哪个链表中,为 EVLIST_*的多种组合
};

libvent 通过使用链表来管理所有的 event,struct event 中的三个链表节点用于插入到聊表中,event_set 用于将 struct event 中的各个字段初始赋值

//设置并初始化 event
//ev 注册的 event
// fd 文件描述符,如果是 timeout event 则忽略该参数
//events 关心的事件 EV_TIMEOUT, EV_SIGNAL, EV_READ, or EV_WRITE 可以通过或运算符同时关心多个事件,发生
// 对应事件 callback 将会被调用
//callback 回调函数,被回调时 fd 和 arg 会传给它 callback(fd, arg)
//arg 传给 callback 的自定义参数
void
event_set(struct event *ev, int fd, short events,
void (*callback)(int, short, void *), void *arg)
{
/* Take the current base - caller needs to set the real base later */
ev->ev_base = current_base; //默认对给全局 reactor 实例进行 io 分发 ev->ev_callback = callback;
ev->ev_arg = arg;
ev->ev_fd = fd;
ev->ev_events = events;
ev->ev_res = 0;
ev->ev_flags = EVLIST_INIT;
ev->ev_ncalls = 0;
ev->ev_pncalls = NULL;
min_heap_elem_init(ev); /* by default, we put new events into the middle priority */
if(current_base)
ev->ev_pri = current_base->nactivequeues/2;
}

然后将 event 真正的添加的内核中,根据 event 中的 ev_flags 中的标志位获知该 event 监听的事件类型为哪几种,插入到对应的数据结构中,下面的代码做了部分裁剪,其中如果监听 EV_READ、EV_WRITE 或者 EV_SIGNAL 则丢给 OS 底层相关,如果为超时则通过 event_queue_insert 插入到 EVLIST_TIMEOUT 中,本节部分我们只要关注 timeout,其他的先暂时不要理会,event_add 参数 tv 指定的是参数 ev 在多少时间后超时回调

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; //底层与 OS 相关的 I/O 分发模式
void *evbase = base->evbase;//底层与 OS 相关的 I/O 分发模式参数
int res = 0;
...
//如果该事件有超时选项(tv 不为 NULL)
//预先在二叉堆中预留一个空位给新添加的 event
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 */
} if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) && //如果监听有非超时意外 event
!(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) { //且未插入到 libevent
res = evsel->add(evbase, ev);//添加到 OS 的 IO 分发 reactor
if (res != -1)
event_queue_insert(base, ev, EVLIST_INSERTED);
} /*
* we should change the timout state only if the previous event
* addition succeeded.
*/
if (res != -1 && tv != NULL) {
struct timeval now;
....
gettime(base, &now);
evutil_timeradd(&now, tv, &ev->ev_timeout); event_debug((
"event_add: timeout in %ld seconds, call %p",
tv->tv_sec, ev->ev_callback)); event_queue_insert(base, ev, EVLIST_TIMEOUT); //添加到超时队列中
} return (res);
}

看一下 event_queue_insert 中插入 EVLIST_TIMEOUT 超时 event 的过程,它将该 event 插入到 base 中的 timeheap 中,如下代码所以,可以看到 event_queue_insert 函数根据标志参数 queue 插入到 base 中不同的字段数据结构中,我们此处关心 EVLIST_TIMEOUT 就可以。现在我们已经成功的将一个 timeout event 添加到了 libevent 当中,之后就是分发阻塞等待被回调

void
event_queue_insert(struct event_base *base, struct event *ev, int queue)
{
if (ev->ev_flags & queue) {
/* Double insertion is possible for active events */
if (queue & EVLIST_ACTIVE)
return; event_errx(1, "%s: %p(fd %d) already on queue %x", __func__,
ev, ev->ev_fd, queue);
}
if (~ev->ev_flags & EVLIST_INTERNAL)
base->event_count++; ev->ev_flags |= queue;
switch (queue) {
case EVLIST_INSERTED:
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;
}
default:
event_errx(1, "%s: unknown queue %x", __func__, queue);
}
}

4.1.3 dispatch 进入循环,分发回调

将事件注册好后我们就可以进行 dispatch 进入循环,当有事件发生的时候(计时器超时),我们注册的回调函数就会被调用,跟踪 event_dispatch 函数进去,最后的逻辑部分在 event_base_loop, 其重要过程为:

  • 获取堆中堆顶 event(时间最靠前最早超时)的超时时间 tv
  • 调用 OS 的 I/O 分发并传递 tv,超时时间为 tv(先不用在意底层是做什么,可能它直接调用 sleep(tv)也说不定)
  • OS I/O 分发结束,从堆中取出所有比当前时间小(超时)的元素,插入到 libevent 的活动队列 base->active_queues
  • 对 base->active_queues 中的 events 进行调用

event_base_loop 的具体实现:

int
event_base_loop(struct event_base *base, int flags)
{
const struct eventop *evsel = base->evsel;
void *evbase = base->evbase;
struct timeval tv;
struct timeval *tv_p;
int res, done;
....
done = 0;
while (!done) {
//省略
.....
timeout_correct(base, &tv); tv_p = &tv;
if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
//获取到 base->timeheap 中最先超时的时间
//如果没有 tv_p 被赋值 NULL,注意参数是 timeval**
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 (!event_haveevents(base)) {//内核中没用要监听的事件退出
event_debug(("%s: no events registered.", __func__));
return (1);
} /* update last old time */
gettime(base, &base->event_tv); /* clear time cache */
base->tv_cache.tv_sec = 0;
//调用 OS 的 I/O 分发,tv_p 表示超时的时间(如果不为 NULL)
//比如如果 OS 的 I/O 分发采用 select,那么 tv_p 相当告诉 select 超时的时间,即正好是我们
//添加到 base->timeheap 最先超时的 event 的时间(最小堆堆顶时间最靠前)
res = evsel->dispatch(base, evbase, tv_p);
if (res == -1)
return (-1);
gettime(base, &base->tv_cache);
//处理超时的 event,通过获取最小堆堆顶与当前时间比较是否超时
//如果超时则将 event 插入到 base->activequeues 并将 base->event_count_active 加 1
timeout_process(base); if (base->event_count_active) { //如果有活动 event
event_process_active(base); //处理活动 event
if (!base->event_count_active && (flags & EVLOOP_ONCE))
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
} /* clear time cache */
base->tv_cache.tv_sec = 0;
return (0);
}

timeout_process 即是从堆中取出所有超时 event 插入到 base->actice_queue 中

void
timeout_process(struct event_base *base)
{
struct timeval now;
struct event *ev;
if (min_heap_empty(&base->timeheap))
return;
gettime(base, &now);
//取出所有超时 event
while ((ev = min_heap_top(&base->timeheap))) {
//与当前时间比较,如果大于当前时间
//则说明没用超时的 event
if (evutil_timercmp(&ev->ev_timeout, &now, >))
break; /* delete this event from the I/O queues */
event_del(ev); event_debug(("timeout_process: call %p",
ev->ev_callback));
//插入到活动队列中
event_active(ev, EV_TIMEOUT, 1);
}
}

4.1.4 兼备事件优先级,统一处理回调

可以从 event_base_loop 中看到处理回调的过程的代码片

if (base->event_count_active) { //如果有活动 event
event_process_active(base); //处理活动 event
if (!base->event_count_active && (flags & EVLOOP_ONCE))
done = 1;
}

如果我们关心的事件发生了(read、write、timeout or singnal),event_process_active 将会调用处理回调,其中 base->event_count_active 指明事件到来的总数,在这里我们就随带将一下 libevent 中回调优先级的过程,注册到 libevent 的 event 是可以带优先级的,优先级最高最先调用,假如活动队列中总是有一个或者几个 event 优先级高于其他的 event,那么低优先级的 event 的将永远也不会被 callback。具体看代码与注释:

static void
event_process_active(struct event_base *base)
{
struct event *ev;
struct event_list *activeq = NULL;
int i;
short ncalls;
//取得数组 nactivequeues 最靠前的一个非 NULL 元素
//即优先级最大的一个活动队列
for (i = 0; i < base->nactivequeues; ++i) {
if (TAILQ_FIRST(base->activequeues[i]) != NULL) {
activeq = base->activequeues[i];
break;
}
}
assert(activeq != NULL);
//一次对该最大优先级队列的 event 进行回调
//优先级小的等到下次被调用时处理
for (ev = TAILQ_FIRST(activeq); ev; ev = TAILQ_FIRST(activeq)) {
//忽略....
ncalls = ev->ev_ncalls;
ev->ev_pncalls = &ncalls;
while (ncalls) {
ncalls--;
ev->ev_ncalls = ncalls;
//回调
(*ev->ev_callback)((int)ev->ev_fd, ev->ev_res, ev->ev_arg);
//...
}
ev->ev_pncalls = NULL;
}
}

event_process_actice 中每次只处理 base->activequeues 数组中的一个链表,这样保证了优先级大的 event 总是先比优先级小的 event 被调用,举个例子:将 1s timeout 优先级为 0 的 ev1 与 1s timeout 优先级为 1 的 ev2 同时 event_add 到 libevent 中,假如他们的回调 callback 被调用之后都会再次将自己注册到 libevent 中(如 timeout 例子中的 timeout_cb 在回调中注册 event),那么不论过了多长时间 ev2 的回调也不会被执行,即使 ev2 已经超时被插入到 activequeues 中。因为他们的超时时间都为 1s,超时之后 ev1 被插在 base-》activequeues[ 0 ],ev2 插在 base->activequeues[ 1 ]中,只有 base->activequeues[ 0 ]为 NULL,base->activequeues[ 1 ]的 event 才会被回调。如果该部分不太清楚的话可以看源代码中 test 目录下 regress.c 测试代码中 test_priorities 函数,里面主要功能就是对优先级队列的测试与验证。

4.2 I/O 事件-监控文件描述符(socket、file etc)

 

4.2.1 I/O event 的注册

libevent 支持网络 IO,检测某个文件描述符是否有事件触发然后回调用户提供的 callback,基于对网络 I/O 事件的监测与分发,libevent 提供了 DNS,HTTP Server,RPC 等组件(libevent2 中将这些组件独立出来)。对网络 IO 事件的分发与 timeout 的整个多长基本相同,libevent 管理 event 使用了 3 中链表分别是 EVLIST_INSERTED, EVLIST_ACTIVE, EVLIST_TIMEOUT。对网络 IO event 的管理只用到前两种。对 IO event 的初始化与 timeout 基本相同,在添加 I/O event 的时候,通过 event->ev_events 判断监测的事件是否有 I/O,如果有的话将其添加到 OS 层进行处理(比如使用 select 判读可读可写),然后插入到 base->eventqueue 中,可以看到 base->eventqueue 不管理 timeout event,它主要保存 IO event 与 singnal event。

if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) && //如果监听有非超时意外 event
!(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) { //且未插入到 libevent
res = evsel->add(evbase, ev);//添加到 OS 的 IO 分发 reactor
if (res != -1)
event_queue_insert(base, ev, EVLIST_INSERTED);
}

evsel->add 执行的操作之前我们没用讲到,现在选取大家比较熟悉的 select I/O 多路复用模式进行分析,从前面的 event_base 结构体定义中可以看到,base->evsel 是一个指向与 os 相关 eventop 结构体,看一下 struct eventop 以及源文件 select.c 下的 selectop 定义:

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;
};
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;
};
const struct eventop selectops = {
"select",
select_init,
select_add,
select_del,
select_dispatch,
select_dealloc,
0
};

在添加 io event 的时候 evsel->add 函数指针实际指向的就是 select_add,在初始化 libevent 时 event_base_new(void)函数中 base->evbase = base->evsel->init(base)调用的的 init 实际调用的就是 select_init,select_init 返回了一个 void*指针,该 void*指针主要与底层 OS select 相关,上层代码不需要关心,select_init 初始化部分数据结构,然后返回一个 selectop 结构体:

struct selectop {
//event_fds 是传给 OS API select 函数的第一个参数,也就是传递给 select 的所用 set 里面
//最大的一个 socket 值+1,因此每次 select_add 的时候都会对 ev->ev_fd 进行判断
int event_fds; /* Highest fd in fd set */
int event_fdsz;
fd_set *event_readset_in;
fd_set *event_writeset_in;
fd_set *event_readset_out;
fd_set *event_writeset_out;
struct event **event_r_by_fd;
struct event **event_w_by_fd;
};
static void *
select_init(struct event_base *base)
{
struct selectop *sop;
/* Disable select when this environment variable is set */
if (evutil_getenv("EVENT_NOSELECT"))
return (NULL);
if (!(sop = calloc(1, sizeof(struct selectop))))
return (NULL);
select_resize(sop, howmany(32 + 1, NFDBITS)*sizeof(fd_mask));
evsignal_init(base);//信号捕捉相关
return (sop);
}

select_init 返回的 void*保存在了 base->evbase 中,调用 struct eventop 函数指针的时候,除 init 外都会将 base->evbase 传递给 struct eventop 的函数指针。evsel->add 调用的是 select_add,select_add 的代码与注释:

//arg 是 select_init 返回的 void*
//ev read、write 或者 singal,也可以使他们中的任意组合比如 ev 同时监测某个 socket 的读/写
static int
select_add(void *arg, struct event *ev)
{
struct selectop *sop = arg; if (ev->ev_events & EV_SIGNAL) //信号的捕捉集成在每一个 os i/o 中
return (evsignal_add(ev)); //直接调用 singnal.c 模块 check_selectop(sop);
/*
* Keep track of the highest fd, so that we can calculate the size
* of the fd_sets for select(2)
* selectop->event_fds 是传给 API select 的第一参数+1,
* 所有添加进来的 fds 中值最大
*/
if (sop->event_fds < ev->ev_fd) {
int fdsz = sop->event_fdsz;
//begin{{{ 内存操作相关,浪费时间的话略过
//一个 fd_mask 有 32 位,内保存 32 个文件描述符
if (fdsz < sizeof(fd_mask))
fdsz = sizeof(fd_mask); //howmany(x,y)将 x 向上取整 y 的倍数
while (fdsz <
(howmany(ev->ev_fd + 1, NFDBITS) * sizeof(fd_mask)))//加一个 ev_fd 进来会不会放不下
fdsz *= 2; if (fdsz != sop->event_fdsz) {
if (select_resize(sop, fdsz)) {//将 selectop 中的各个 set 的缓冲区扩充
check_selectop(sop);
return (-1);
}
}
//}}}end 内存操作相关 //添加进来的 fds 文件描述符大于当前保存的文件描述符
//更新 sop->event_fds
sop->event_fds = ev->ev_fd;
} //将不同 ev 的文件描述符添加到 select 的不同 set 中
//读 event 添加到 readset,写 event 添加到 writeset
//将文件描述符作为索引,保存到 selectop->event_*_by_fd[ev->ev_fd]=ev 中
if (ev->ev_events & EV_READ) {
FD_SET(ev->ev_fd, sop->event_readset_in);
sop->event_r_by_fd[ev->ev_fd] = ev;
}
if (ev->ev_events & EV_WRITE) {
FD_SET(ev->ev_fd, sop->event_writeset_in);
sop->event_w_by_fd[ev->ev_fd] = ev;
}
check_selectop(sop); return (0);
}

其中的内存相关操作部分可以不用看,理解起来也不难,知道howmany的作用就可以,类似STL内存池里面向上取整操作,《深入理解操作系统》前面部分有C语言相关位操作的章节(仅仅看了前面部分)

Author: liangsijian

Created: 2016-04-23 周六 13:00

Emacs 25.0.92.1 (Org mode 8.2.10)

Validate

libevent 网络IO分析的更多相关文章

  1. libevent源码分析一--io事件响应

    这篇文章将分析libevent如何组织io事件,如何捕捉事件的发生并进行相应的响应.这里不会详细分析event与event_base的细节,仅描述io事件如何存储与如何响应. 1.  select l ...

  2. 网络IO之阻塞、非阻塞、同步、异步总结

    网络IO之阻塞.非阻塞.同步.异步总结 1.前言 在网络编程中,阻塞.非阻塞.同步.异步经常被提到.unix网络编程第一卷第六章专门讨论五种不同的IO模型,Stevens讲的非常详细,我记得去年看第一 ...

  3. libevent源码分析:hello-world例子

    hello-world是libevent自带的一个例子,这个例子的作用是启动后监听一个端口,对于所有通过这个端口连接上服务器的程序发送一段字符:hello-world,然后关闭连接. /* * gcc ...

  4. Socket-IO 系列(一)Linux 网络 IO 模型

    Socket-IO 系列(一)Linux 网络 IO 模型 一.基本概念 在正式开始讲 Linux IO 模型前,先介绍 5 个基本概念. 1.1 用户空间与内核空间 现在操作系统都是采用虚拟存储器, ...

  5. 网络IO之阻塞、非阻塞、同步、异步总结【转】

    1.前言 在网络编程中,阻塞.非阻塞.同步.异步经常被提到.unix网络编程第一卷第六章专门讨论五种不同的IO模型,Stevens讲的非常详细,我记得去年看第一遍时候,似懂非懂,没有深入理解.网上有详 ...

  6. 网络IO

    1.前言 在网络编程中,阻塞.非阻塞.同步.异步经常被提到.unix网络编程第一卷第六章专门讨论五种不同的IO模型,Stevens讲的非常详细,我记得去年看第一遍时候,似懂非懂,没有深入理解.网上有详 ...

  7. 高并发之网络IO模型

    你好,我是坤哥 今天我们聊一下高并发下的网络 IO 模型 高并发即我们所说的 C10K(一个 server 服务 1w 个 client),C10M,写出高并发的程序相信是每个后端程序员的追求,高并发 ...

  8. 【转】libevent源码分析

    libevent源码分析 转自:http://www.cnblogs.com/hustcat/archive/2010/08/31/1814022.html 这两天没事,看了一下Memcached和l ...

  9. Libevent源码分析 (1) hello-world

    Libevent源码分析 (1) hello-world ⑨月份接触了久闻大名的libevent,当时想读读源码,可是由于事情比较多一直没有时间,现在手头的东西基本告一段落了,我准备读读libeven ...

随机推荐

  1. U3D的有限状态机系统

    或许广大程序员之前接触过游戏状态机,这已不是个新鲜的词汇了.其重要性我也不必多说了,但今天我要讲到的一个状态机框架或许您以前并未遇到过.所以,我觉得有必要将自己的心得分享一下.下面是一个链接:http ...

  2. DOM的学习

    今天学习了DOM,感觉学习起来真的没那么简单啦,这不是一个好现象啊,只有依靠自己大补课,嘿嘿,具体的总结了一下,今天学习的其实并不多,仅仅学习了不同的节点类型,但是知识还是蛮碎的,要一点一点的总结,昨 ...

  3. 【摘抄】C++程序员练级攻略

    摘抄自互联网文章 作为C++程序员,或者说程序员一定要提升自己: 专访李运华:程序员如何在技术上提升自己-CSDN.NET专访徐宜生:坚决不做代码搬运工!-CSDN.NET 上面两个文章我觉得都不错. ...

  4. Spring系列之IOC容器

    一.概述 IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化.定位.配置应用程序中的对象及建立这些对象之间的依赖.应用程序无需直接在代码中new 相关的对象,应用程序由IOC容器进行组装.在S ...

  5. bigdecimal 与long int 之间转换

    BigDecimal与Long.int之间的互换 在实际开发过程中BigDecimal是一个经常用到的数据类型,它和int Long之间可以相互转换. 转换关系如下代码展示: int 转换成 BigD ...

  6. OpenCV——轮廓特征描述

    检测出特定轮廓,可进一步对其特征进行描述,从而识别物体. 1. 如下函数,可以将轮廓以多种形式包围起来. // 轮廓表示为一个矩形 Rect r = boundingRect(Mat(contours ...

  7. 【cs229-Lecture14】主成分分析法

    本节课内容: 因子分析 ---因子分析中的EM步骤的推导过程 主成份分析:有效地降低维度的方法 因子分析 混合高斯模型的问题 接下来讨论因子分析模型,在介绍因子分析模型之前,先看高斯分布的另一种写法, ...

  8. PHP魔术变量和魔术方法

    基础知识:魔术变量和魔术方法 魔术变量:最初PHP魔术变量的出现主要是为了方便开发者调试PHP的代码;当然也可以利用这个实现特殊需求.在写法上魔术变量前后都有两个下划线. 如:_LINE_:返回文件中 ...

  9. C语言程序设计--输入与输出

    C语言的输入 所有的输入都是依赖于C语言函数进行的,这个函数是C语言标准库自带的,定义在头文件<stdio.h>里面,所以,要想使用与输入相关的函数,都需要包含这个头文件 #include ...

  10. 配置Mac漂亮的Shell--Iterm2+OhMyZSH+Agnoster

    安装包管理器 首先当然是解决包管理的问题,Mac下面是Homebrew的天下了 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubuserco ...