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. MySQL 安装与配置

    Linux 安装 MySQL Windows 安装 MySQL 如何连接 MySQL 如何修改 MySQL 密码 如何重置 MySQL 密码

  2. Splash runjs() 方法

    runjs() 方法可以执行 JavaScript 代码,它与 evaljs() 功能类似,但是更偏向于执行某些动作或声明某些方法 function main(splash, args) splash ...

  3. Hibernate系列之核心开发接口

    一.概述 所有的hibernate应用中都会访问5个核心接口,它们分别是: Configuration:配置hibernate,创建SessionFactory对象 SessionFactory:初始 ...

  4. Linux调试分析诊断利器——strace

    strace是个功能强大的Linux调试分析诊断工具,可用于跟踪程序执行时进程系统调用(system call)和所接收的信号,尤其是针对源码不可读或源码无法再编译的程序. 在Linux系统中,用户程 ...

  5. SQL SERVER 2008 R2安全配置与防暴力破解

    https://blog.csdn.net/enweitech/article/details/49864215 0x00 sql server 2008 权限介绍 在访问sql server 200 ...

  6. ThinkPad L421 如何进入BIOS?(已解决)

    开机屏幕出现ThinkPad标志时,快速按下 F1键 即可进入BIOS

  7. 怎么使用jstack精确找到异常代码

    1.代码demo //一个CPU密集型线程的demo: package chapter1; public class FindJavaThreadInTaskManager { public stat ...

  8. html5media 视频

    官网: https://html5media.info/ 二.引入script <script src="//api.html5media.info/1.1.8/html5media. ...

  9. Yet another way to manage your NHibernate ISessionFactory

    So here is my current UnitOfWork implementation.  This one makes use of the somewhat new current_ses ...

  10. IOS控制系统手势返回

    self.navigationController.interactivePopGestureRecognizer.enabled = YES; //手势返回的代理,如果自定义了leftButtonI ...