libevent源码分析之关于IO复用的选取
2015-06-21 10:54
330 查看
关于libevent使用的后台IO复用技术, 无非就是那几种,
这里我们将会看到的是libevent是如何选取某平台上最合适的IO复用的, 以及对IO复用的包装的例子
想要了解后台IO复用的选取, 首先需要知道的是在哪里声明了IO复用的接口, 既然IO复用是要贯穿整个网络库的, 那么说明我们需要从event_base里找:
果然找到了, 注释中写到的backend就是我们要找的后端技术IO复用, 以及下面void*类型的 evbase,为不同IO复用需要用到的一些不同的数据, 虽然需要的数据各不相同,但我们可以像给线程传数据一样, 使用结构体来作为传递数据的媒介.
接下来我们继续循着找到的线索, 寻找struct eventop类型是怎样的存在:
将来若是要对IO复用添加或删除描述符等操作, 只需要通过evsel调用并传入evbase参数就可以了!
当然, 仅仅知道外部接口远远不够, 我们需要深入下去. 既然我们知道了如何抽象的使用IO复用,那对于那么多IO复用技术,libevent是如何进行选择的呢?
这时候我们就要找哪个文件夹包含了几乎所有的IO复用的名称, 在event.c中我们看到:
(假如某平台支持某种IO复用技术,那么在config.h(或其他配置文档)中就会事先定义好表示支持该IO复用的宏, 于是有了如下内容)
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复用的选择:
从这里我们知道了libevent是如何进行IO复用技术的选择的, 现在为止, 我们了解了如何选用IO复用, 如何抽象的使用, 大致齐全了, 于是我们可以更深入下去, 看看libevent是如何封装这些IO复用的!
通过观察libevent库的所有文档,可以发现其为IO复用技术分别封装在了不同的文档, 比如select.c, kqueue.c , epoll.c等
现在我们具体观察一下较为简单的select.c
下面,我们就具体的看看每一个操作是如何封装的!
首先是在刚刚在event_base_new_with_config中看到的能返回特定数据的初始化函数
到这里分析就结束了,还是很感谢此博客博主:http://blog.csdn.net/luotuo44/article/details/38458469
这里我们将会看到的是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
相关文章推荐
- C#高级编程十九天----部分类
- C#高级编程十八天----C#中的结构
- python解决数据预处理:将KDDCPU99数据格式转换成libsvm可读的格式
- Java中的clone方法详解
- 二进制文件浏览器
- Python 发送邮件
- PPII打不开 更改I.bat
- win7怎样重新安装卸载之后的Windows系统更新与补丁?
- 最短路径
- 数据库 三范式最简单最易记的解释
- 最短路径 分类: 图论 2015-06-21 10:52 16人阅读 评论(0) 收藏
- 启动 Eclipse 弹出“Failed to load the JNI shared library jvm.dll”错误的解决方法!
- 计算机操作系统总结
- 大数据导出错误
- Linux使用jstat命令查看jvm的GC情况
- 解决PHP后端生成的图片无法使用CDN缓存的方法
- JS实现直接选择排序
- QT设置窗口背景
- 修复ubuntu14.04的grub的方法
- 营销策略之信任流量【流量3.0时代】