关于libevent使用的后台IO复用技术, 无非就是那几种,
这里我们将会看到的是libevent是如何选取某平台上最合适的IO复用的, 以及对IO复用的包装的例子
想要了解后台IO复用的选取, 首先需要知道的是在 哪里声明了IO复用的接口, 既然IO复用是要贯穿整个网络库的, 那么说明我们需要从event_base里找:
struct event_base {
/** Function pointers and other data to describe this event_base's
* backend. */
const struct eventop *evsel;
/** Pointer to backend-specific data. */
void *evbase;
//...
}
果然找到了, 注释中写到的backend就是我们要找的后端技术IO复用, 以及下面void*类型的 evbase,为不同IO复用需要用到的一些不同的数据, 虽然需要的数据各不相同,但我们可以像给线程传数据一样, 使用结构体来作为传递数据的媒介.
接下来我们继续循着找到的线索, 寻找struct eventop类型是怎样的存在:
/** Structure to define the backend of a given event_base. */
struct eventop {
//IO复用的名称, 比如select ,epoll等
const char *name;
//初始化此IO复用
void *(*init)(struct event_base *);
//往IO复用中添加要监听的描述符
int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
//开始监听
int (*dispatch)(struct event_base *, struct timeval *);
//释放IO复用
void (*dealloc)(struct event_base *);
//是否需要重新初始化IO复用
int need_reinit;
//IO复用的特性, 比如某些IO复用支持ET模式
enum event_method_feature features;
//此类IO复用需要用到的特定数据, 比如select需要用到的几个fd_set, fd_max等
size_t fdinfo_len;
};
好了, 现在知道了原来是将IO复用的接口封装在了eventop结构体中
将来若是要对IO复用添加或删除描述符等操作, 只需要通过evsel调用并传入evbase参数就可以了!
当然, 仅仅知道外部接口远远不够, 我们需要深入下去. 既然我们知道了如何抽象的使用IO复用,那对于那么多IO复用技术,libevent是如何进行选择的呢?
这时候我们就要找哪个文件夹包含了几乎所有的IO复用的名称, 在event.c中我们看到:
(假如某平台支持某种IO复用技术,那么在config.h(或其他配置文档)中就会事先定义好表示支持该IO复用的宏, 于是有了如下内容)
#ifdef _EVENT_HAVE_EVENT_PORTS
extern const struct eventop evportops;
#endif
#ifdef _EVENT_HAVE_SELECT
extern const struct eventop selectops;
#endif
#ifdef _EVENT_HAVE_POLL
extern const struct eventop pollops;
#endif
#ifdef _EVENT_HAVE_EPOLL
extern const struct eventop epollops;
#endif
#ifdef _EVENT_HAVE_WORKING_KQUEUE
extern const struct eventop kqops;
#endif
#ifdef _EVENT_HAVE_DEVPOLL
extern const struct eventop devpollops;
#endif
#ifdef WIN32
extern const struct eventop win32ops;
#endif
/* 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 //在linux平台下的所有IO复用技术中, 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
};
struct eventop我们在event_base中看到, 这是用于抽象IO复用的结构体, 以上内容的意思就是只要支持该种类型IO复用技术,就以其名称定义一个可用的eventop结构体, 然后放到eventops数组中, 等待将来被挑选. 那么是如何进行挑选的呢, 接下来我们就看看. 只是会在哪里进行选择呢?在eventop中的init函数?此函数是用于初始化IO复用的,可能,但是并未找到. 那么估计在event_base的初始化函数中?
event_base_new也只是简单的调用event_base_new_with_config,于是我们在event_base_new_with_config中找到了 IO复用的选择:
//其中省略了无关部分
struct event_base *
event_base_new_with_config(const struct event_config *cfg)
{
... ...
//首先我们关注的是最外层的for循环, i初始为0, 表示从eventops的第一个元素开始进行筛选合适的IO复用技术.从循环条件看出, 一旦选出合适的就选用而不再继续寻找
//按照自己的思维, 选择IO复用肯定选最优的, 比如在linux下一般首选epoll, 这么一来, 我们再回到eventops的初始化处,发现果然epoll在其他复用技术的前面.
//cfg我们不具体认识,假设它是存在的,
//关于event_config_is_avoided_method函数其实是字符串对比函数, 而cfg是对event_base的基本参数设置, 其中可能声明了不允许使用某种IO复用技术
//于是这里根据参数设置排除选择某种IO复用, 另外选择合适的
//cfg->require_features从字面意思就可以看出,指的是要求IO复用技术具备某种特性,比如支持ET模式等, 若不具备此特性就会被放弃使用
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];
//这里我们发现调用了该复用技术的init函数, 得到了该函数需要的特定参数
base->evbase = base->evsel->init(base);
}
if (base->evbase == NULL) {
event_warnx("%s: no event mechanism available",
__func__);
base->evsel = NULL;
event_base_free(base);
return NULL;
}
//声明了这个宏后, libevent会告诉用户使用了哪种IO复用技术
if (evutil_getenv("EVENT_SHOW_METHOD"))
event_msgx("libevent using: %s", base->evsel->name);
... ...
return (base);
}
从这里我们知道了libevent是如何进行IO复用技术的选择的, 现在为止, 我们了解了 如何选用IO复用, 如何抽象的使用, 大致齐全了, 于是我们可以更深入下去, 看看libevent是如何封装这些IO复用的!
通过观察libevent库的所有文档,可以发现其为IO复用技术分别封装在了不同的文档, 比如select.c, kqueue.c , epoll.c等
现在我们 具体观察一下较为简单的select.c
//观察一下此结构体的内容,我们就可以知道这里面装的是select函数所需要的参数(即特定数据):
struct selectop {
int event_fds; /* Highest fd in fd set */
int event_fdsz; //这是每个fd_set的长度, 有时候可能要监听的描述符太多,装不下从而需要更多的空间去装
int resize_out_sets;
fd_set *event_readset_in; //这是可读集合, 用过select的都明白
fd_set *event_writeset_in; //可写集合
fd_set *event_readset_out; //以下这两个干嘛用的呢, 他们就是用来当作参数传入select的,因为每次传入的集合返回时都会被select内部修改,所以我们拷贝一份set传给select, 下次继续调用select时再拷贝一份传入
fd_set *event_writeset_out;
};
//下面就是eventop结构体了, 是对IO复用的抽象
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 *);
const struct eventop selectops = {
"select", //名字
select_init,
select_add,
select_del,
select_dispatch,
select_dealloc,
0, /* doesn't need reinit. */
EV_FEATURE_FDS, //该标志表示既能支持文件描述符,也能支持套接字描述符
0,
};
/*最后一个参数为0是这么来的,与poll相比较:
poll需要一段额外的内存,以避免同一个文件描述符被重复插入IO复用机制的事件表中。select却不需要这个额外内存,是因为select使用的是fd_set,可以看作是一组文件描述字(fd)的集合,它用一位来表示一个fd, 即使遇到相同的也只是无意义的覆盖。但是对于poll来说就不同了,poll存储fd的方式是一个fd对应一个结构体,所以需要一种方式处理这种重复出现fd的错误情况*/
下面,我们就具体的看看每一个 操作是如何封装的!
首先是在刚刚在event_base_new_with_config中看到的能返回特定数据的初始化函数
static void *
select_init(struct event_base *base)
{
struct selectop *sop; //先声明一个特定数据结构体指针
if (!(sop = mm_calloc(1, sizeof(struct selectop))))
return (NULL);
//因为在selectop结构体中的fd_set是个指针,知道了fd_set原理这个也就轻松理解了.
//第一次运算得到值是8,即2个long类型, 可存64(2*4*8,2个long,每个4字节,每个字节8位)个fd
if (select_resize(sop, SELECT_ALLOC_SIZE(32 + 1))) {
select_free_selectop(sop);
return (NULL);
}
//下面是IO复用对信号的处理,如果接触过统一事件源处理的话,可以轻松理解为什么在IO复用处需要信号初始化, 不过仍会在将来的libevent信号处理章节中进行讲解
evsig_init(base);
return (sop);
}
//resize函数指的是重新调整存储描述符的set的大小
//在看init中的resize时, 不要为没有给readset_out和writeset_out分配空间而疑惑,因为他们是拷贝readset_in和writeset_in的, 所以会在启动select的时候给他们分配
//在非init函数的地方调用resize是希望扩展空间,所以用realloc
static int
select_resize(struct selectop *sop, int fdsz)
{
fd_set *readset_in = NULL;
fd_set *writeset_in = NULL;
if (sop->event_readset_in)
check_selectop(sop);
if ((readset_in = mm_realloc(sop->event_readset_in, fdsz)) == NULL)
goto error;
sop->event_readset_in = readset_in;
if ((writeset_in = mm_realloc(sop->event_writeset_in, fdsz)) == NULL) {
/* Note that this will leave event_readset_in expanded.
* That's okay; we wouldn't want to free it, since that would
* change the semantics of select_resize from "expand the
* readset_in and writeset_in, or return -1" to "expand the
* *set_in members, or trash them and return -1."
*/
goto error;
}
sop->event_writeset_in = writeset_in;
//这里resize_out_sets指的是此时read_out和write_out也需要resize
sop->resize_out_sets = 1;
//将新申请的内存空间置0
memset((char *)sop->event_readset_in + sop->event_fdsz, 0,
fdsz - sop->event_fdsz);
memset((char *)sop->event_writeset_in + sop->event_fdsz, 0,
fdsz - sop->event_fdsz);
//新的集合(fd_set)的大小
sop->event_fdsz = fdsz;
check_selectop(sop);
return (0);
error:
event_warn("malloc");
return (-1);
}
//接着看看如何处理添加要监听的描述符的
static int
select_add(struct event_base *base, int fd, short old, short events, void *p)
{
struct selectop *sop = base->evbase; //得到特定数据的结构体对象
(void) p;
EVUTIL_ASSERT((events & EV_SIGNAL) == 0);
check_selectop(sop);
/*
* Keep track of the highest fd, so that we can calculate the size
* of the fd_sets for select(2)
*/
if (sop->event_fds < fd) { //如果highest fd小于新添加的fd, 那我们想到的肯定是另highest fd=fd,且此时可能描述符集合已经存不下了
//因为是按位存储,所以当sop->event_fds > fd的时候是肯定不要扩展集合的,即肯定是存的下的
int fdsz = sop->event_fdsz;
if (fdsz < (int)sizeof(fd_mask))
fdsz = (int)sizeof(fd_mask);
/* In theory we should worry about overflow here. In
* reality, though, the highest fd on a unixy system will
* not overflow here. XXXX */
while (fdsz < (int) SELECT_ALLOC_SIZE(fd + 1)) //下次可能传入的是64,那么SELECT_ALLOC_SIZE得到的可能就是12,最终fdsz就是16
fdsz *= 2;
if (fdsz != sop->event_fdsz) { //发现要调整
if (select_resize(sop, fdsz)) {
check_selectop(sop);
return (-1);
}
}
sop->event_fds = fd;
}
if (events & EV_READ)
FD_SET(fd, sop->event_readset_in);
if (events & EV_WRITE)
FD_SET(fd, sop->event_writeset_in);
check_selectop(sop);
return (0);
}
//最后再看一个IO复用的启动函数封装
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);
//既然原版resize了, 那么拷贝的也就需要resize
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; //调整完毕,下次不需要调整了除非在add函数中又发生了调整
}
//实施复制
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;
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);
//得到了select返回的结果后,首先我们知道select的麻烦在于其返回后的O(N)复杂度,这里从N中随机取一个数开始循环,而不是每次都从0开始
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;
evmap_io_active(base, i, res);
}
check_selectop(sop);
return (0);
}
到这里分析就结束了,还是很感谢此博客博主:http://blog.csdn.net/luotuo44/article/details/38458469