libevent源码分析之关于IO复用的选取

关于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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值