前言
分析libevent库就不得不先了解I/O多路复用技术,这是Reactor模式实现的基础,而Reactor模式是libevent库的核心内容。
那么何为I/O多路复用技术? 可以参考http://blog.163.com/xychenbaihu@yeah/blog/static/13222965520112163171778/的介绍,我个人理解所谓I/O多路复用技术,简单地说就是当应用中要“同时”监视多个I/O描述符状态(可读,可写,异常)的时候,不需要在应用层分别对每个I/O描述符调用I/O操作函数(read, write)进行监视和处理,而是将这些描述符按照监视状态设置成集合,通过一次系统调用实现。借用《UNIX网络编程》第一卷 第6章的说明就是:如果一个或多个I/O条件满足(例如,输入已经准备好被读,或者描述符字可以承接更多的输出)时,我们就被通知到,这个能力被称为I/O复用。 I/O复用典型地用在下列网络应用场合:
- 当客户处理多个描述符字时(一般是交互式输入和网络套接口)
- 一个客户同时处理多个套接口时可能的,但很少出现
- 如果一个TCP服务器即要处理监听套接口,又要处理已链接套接口
- 如果一个服务器即要处理TCP,又要处理UDP
- 如果一个服务器要处理多个服务或者多个协议
第一篇已经介绍了libevent支持的I/O多路复用技术有select/poll/epoll,先从最传统的select函数开始分析。
select函数定义
select是一种同步的I/O多路复用函数,该函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程。所谓同步I/O是区别于异步I/O,即select函数返回要么是因为有错误产生或者超时返回,要么是因为监视的句柄状态就绪,否则该函数会阻塞。关于I/O模型的说明可以参考一下http://www.ibm.com/developerworks/cn/linux/l-async/
在linux环境下man select可以得到该函数的帮助信息如下:
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明
ndfs: select函数参数中三个描述符集中找出最高描述符编号值,然后加1
readfds: select函数监视的可读描述符集合
writefds: select函数监视的可写描述符集合
exceptfds: select函数监视的异常描述符集合
timeout: select函数超时结束时间(timeout == NULL表示永久等待; timeout->tv_sec ==0 && timeout->tv_usec == 0表示完全不等待,测试所有指定的描述符并立即返回)
返回值说明
-1 : 有错误产生
0 : 表示select函数超时返回,且函数监视的可读、可写、异常描述符集合无事件发生
>0 : 表示select函数成功返回,返回值为已经准备好描述符的正数目,且三个描述符集合中仍旧打开的位是对应于已经准备好的描述符位
描述符集合操作相关的函数
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
libevent库中select I/O复用技术的实现
libevent支持多种I/O多路复用技术,这些技术的实现是通过eventop这个结构来抽象的(非常巧妙,C也能实现类似C++的多态),其定义如下:
/** Structure to define the backend of a given event_base. */
struct eventop {
/** The name of this backend. */
const char *name;
/** Function to set up an event_base to use this backend. It should
* create a new structure holding whatever information is needed to
* run the backend, and return it. The returned pointer will get
* stored by event_init into the event_base.evbase field. On failure,
* this function should return NULL. */
void *(*init)(struct event_base *);
/** Enable reading/writing on a given fd or signal. 'events' will be
* the events that we're trying to enable: one or more of EV_READ,
* EV_WRITE, EV_SIGNAL, and EV_ET. 'old' will be those events that
* were enabled on this fd previously. 'fdinfo' will be a structure
* associated with the fd by the evmap; its size is defined by the
* fdinfo field below. It will be set to 0 the first time the fd is
* added. The function should return 0 on success and -1 on error.
*/
int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
/** As "add", except 'events' contains the events we mean to disable. */
int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
/** Function to implement the core of an event loop. It must see which
added events are ready, and cause event_active to be called for each
active event (usually via event_io_active or such). It should
return 0 on success and -1 on error.
*/
int (*dispatch)(struct event_base *, struct timeval *);
/** Function to clean up and free our data from the event_base. */
void (*dealloc)(struct event_base *);
/** Flag: set if we need to reinitialize the event base after we fork.
*/
int need_reinit;
/** Bit-array of supported event_method_features that this backend can
* provide. */
enum event_method_feature features;
/** Length of the extra information we should record for each fd that
has one or more active events. This information is recorded
as part of the evmap entry for each fd, and passed as an argument
to the add and del functions above.
*/
size_t fdinfo_len;
};
具体到select I/O多路复用的实现定义如下:
const struct eventop selectops = {
"select",
select_init,
select_add,
select_del,
select_dispatch,
select_dealloc,
0, /* doesn't need reinit. */
EV_FEATURE_FDS,
0,
};
libevent库的select封装
libevent库中对select的封装在Select.c文件中,其中selectop结构是对描述符数据的封装
struct selectop {
int event_fds; /* Highest fd in fd set */
int event_fdsz; /* 描述符集的存储空间大小 */
int resize_out_sets; /* 标志位,在select_resize函数中对event_readset_in和event_writeset_in两个描述符集调整了存储空间后将该标志位置为1,
在select_dispatch函数中对event_readset_out和event_writeset_out两个描述符集调整了存储空间后将该标志位置为0 */
fd_set *event_readset_in; /* select函数监视的读描述符集,用于select_add,select_del函数操作 */
fd_set *event_writeset_in; /* select函数监视的写描述符集,用于select_add,select_del函数操作 */
fd_set *event_readset_out; /* select函数监视的读描述符集,用于select函数调用操作 */
fd_set *event_writeset_out; /* select函数监视的读描述符集,用于select函数调用操作 */
};
如下几个函数主要是对描述符操作和select函数的封装
static void *select_init(struct event_base *);
static int select_add(struct event_base *, int, short old, short events, void*);
static int select_del(struct event_base *, int, short old, short events, void*);
static int select_dispatch(struct event_base *, struct timeval *);
static void select_dealloc(struct event_base *);
指定采用select
还是习惯通过例程的调试方法来分析上面select这几个相关函数的关系,但不幸的是Linux下默认的多路复用实现不是select,但Linux肯定是支持select,如何让例程采用select呢?
在event_base_new_with_config函数中可以发现libevent选取多路复用机制的实现,按照eventtops中eventop的顺序检查,最先满足的eventop将被采用,所以如果要指定select,就必须把前面满足的项忽略掉
for (i = 0; eventops[i] && !base->evbase; i++) {
if (cfg != NULL) {
/* determine if this backend should be avoided */
if (event_config_is_avoided_method(cfg,
eventops[i]->name))
continue;
if ((eventops[i]->features & cfg->require_features)
!= cfg->require_features)
continue;
}
/* also obey the environment variables */
if (should_check_environment &&
event_is_method_disabled(eventops[i]->name))
continue;
base->evsel = eventops[i];
base->evbase = base->evsel->init(base);
}
而eventops定义的顺序如下:
/* Array of backends in order of preference. */
static const struct eventop *eventops[] = {
#ifdef _EVENT_HAVE_EVENT_PORTS
&evportops,
#endif
#ifdef _EVENT_HAVE_WORKING_KQUEUE
&kqops,
#endif
#ifdef _EVENT_HAVE_EPOLL
&epollops,
#endif
#ifdef _EVENT_HAVE_DEVPOLL
&devpollops,
#endif
#ifdef _EVENT_HAVE_POLL
&pollops,
#endif
#ifdef _EVENT_HAVE_SELECT
&selectops,
#endif
#ifdef WIN32
&win32ops,
#endif
NULL
};
了解清楚这个原理,修改一下例程,即可搞定。
int main(int argc, char** argv)
{
/* Initalize the event library */
struct event_config* pCfg = event_config_new();
event_config_avoid_method(pCfg, "epoll"); // 忽略掉epoll机制
event_config_avoid_method(pCfg, "poll"); // 忽略掉poll机制
struct event_base* base = event_base_new_with_config(pCfg);
。。。。。。
}
现在用gdb调试这个例程,在select_init函数上设置断点就可以OK了
select的几个函数都不复杂,重点分析一下select_dispatch函数
static int
select_dispatch(struct event_base *base, struct timeval *tv)
{
int res=0, i, j, nfds;
struct selectop *sop = base->evbase;
check_selectop(sop);
// 调整event_readset_out和event_writeset_out的存储空间
if (sop->resize_out_sets) {
fd_set *readset_out=NULL, *writeset_out=NULL;
size_t sz = sop->event_fdsz;
if (!(readset_out = mm_realloc(sop->event_readset_out, sz)))
return (-1);
sop->event_readset_out = readset_out;
if (!(writeset_out = mm_realloc(sop->event_writeset_out, sz))) {
/* We don't free readset_out here, since it was
* already successfully reallocated. The next time
* we call select_dispatch, the realloc will be a
* no-op. */
return (-1);
}
sop->event_writeset_out = writeset_out;
sop->resize_out_sets = 0;
}
memcpy(sop->event_readset_out, sop->event_readset_in,
sop->event_fdsz);
memcpy(sop->event_writeset_out, sop->event_writeset_in,
sop->event_fdsz);
nfds = sop->event_fds+1;
// 在select_dispatch的上层调用函数event_base_loop中已经加锁,下面select函数并不访问event_base数据且该函数有可能阻塞,故解锁
EVBASE_RELEASE_LOCK(base, th_base_lock);
res = select(nfds, sop->event_readset_out,
sop->event_writeset_out, NULL, tv);
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
check_selectop(sop);
if (res == -1) {
if (errno != EINTR) {
event_warn("select");
return (-1);
}
return (0);
}
event_debug(("%s: select reports %d", __func__, res));
check_selectop(sop);
// 这里比较有意思,并不是从最小的描述符开始检查,而是做了个随机处理
i = random() % nfds;
for (j = 0; j < nfds; ++j) {
if (++i >= nfds)
i = 0;
res = 0;
if (FD_ISSET(i, sop->event_readset_out))
res |= EV_READ;
if (FD_ISSET(i, sop->event_writeset_out))
res |= EV_WRITE;
if (res == 0)
continue;
// 将已经ready的描述符和对应的注册事件插入event_base的事件队列中
evmap_io_active(base, i, res);
}
check_selectop(sop);
return (0);
}