libevent源码学习----io多路复用的封装和使用
2017-10-12 15:50
507 查看
因为是非阻塞监听事件的发生,所以内部其实还是采用io多路复用函数实现的。
又因为可供选择的io函数很多,linux下有epoll, poll, select等,window下有ICOP, select等,所以libevent需要在程序启动时选择一个合适的io多路复用函数,合适的依据是
系统支持,为了实现跨平台
io函数的效率尽量高
用户是否主动设置了不想使用的io函数
为了解决跨平台,libevent对所有的io函数都进行了各自的封装
为了解决效率问题,libevent在选择时,从效率高的开始选
为了解决用户设置,libevent为每一个io函数提供一个名字,用户人为设置不想使用的io函数时也是传送io函数名字,libevent维护一个字符串队列,选择不在这个队列中的io函数
以下程序就是libevent如何初始化io多路复用函数的
全局io函数数组,以此实现跨平台,根据预编译头判断系统是否支持某个io函数,从而构造出一个存储着所有可用的io多路复用函数的数组,初始化base时,只需要筛选出用户允许的即可
接下来单独看每一个io函数的封装
select的封装,由此可见每个io函数的封装都是定义一个struct eventop类型的变量,将对应io函数的接口指针存储着,这就将所有的io函数都统一起来,不需要特定io调用特定接口
此外libevent也对每个io函数使用的数据类型进行了封装,比如说epoll_event,pollfd以及fd_set
对select的fd_set进行的封装,为什么read/write都有两份,可以参考几种服务器模型以及io多路复用函数中的select部分
对于epoll也是如此,libevent内部epoll有另一种封装,不明白原理
可以发现,每个io多路复用函数的封装都是遵循struct eventop类型的,所以base中只需要存储着eventop类型的指针evsel,在初始化它之后只需要调用struct eventop提供的接口函数,就可以直接调用io多路复用函数的接口函数,实现了统一
而对于每个io多路复用函数的数据类型,libevent没有进行统一的封装,因为也没有必要。在初始化base中
libevent中io多路复用的使用体现在
新建event注册到base中,此时会把监听的fd和事件添加到io复用中,本质上就是调用epoll_ctl,FD_SET等
删除event从base中,会把监听的fd从io复用中删除,本质上调用epoll_ctl等
开启事件驱动循环,监听事件的发生,本质上调用各种wait函数如epoll_wait,select,poll等
比如添加event
其实都是间接调用每一个io接口
总结
这部分主要学习到libevent是如何实现跨平台的io多路复用函数的选择的,所谓跨平台,就是将所有可能的平台都考虑到。同时看到libevent是如何把所有io函数都进行统一的,这一点很值得学习
题外话,其实对io的封装就是基类纯虚函数加各种派生类,用基类指针实现多态….
又因为可供选择的io函数很多,linux下有epoll, poll, select等,window下有ICOP, select等,所以libevent需要在程序启动时选择一个合适的io多路复用函数,合适的依据是
系统支持,为了实现跨平台
io函数的效率尽量高
用户是否主动设置了不想使用的io函数
为了解决跨平台,libevent对所有的io函数都进行了各自的封装
为了解决效率问题,libevent在选择时,从效率高的开始选
为了解决用户设置,libevent为每一个io函数提供一个名字,用户人为设置不想使用的io函数时也是传送io函数名字,libevent维护一个字符串队列,选择不在这个队列中的io函数
以下程序就是libevent如何初始化io多路复用函数的
/* 由event_base_new调用 */ struct event_base * event_base_new_with_config(const struct event_config *cfg) { int i; struct event_base *base; /* ... */ /* * 为了实现让base可以根据系统需要或者用户的需要调用不同的io复用函数, * 比如说系统可能不支持某个io复用函数,又或者是用户指明不想要使用 * 哪个io复用函数,指明不想只要哪个函数可以通过调用带有config的函数提供 * 创建base的配置 * 为了解决这种情况,在全局变量中有一个eventopts数组,这个数组中存储着 * 所有系统支持的io复用函数,每个io复用函数都是一个结构体实例化对象 * 可以在每一个io函数的文件,比如select.c中看到 * 而evbase存储的不是使用的io函数,而是使用的io函数对应的数据结构体 * 其实就是存数据的,在io函数文件中也可以看到 * 下面的for循环是为了找到第一个可用的io函数,因为数组中的函数是按效率 * 排序的 */ base->evbase = NULL; /* 如上 */ 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; } base->evsel = eventops[i]; /* 调用对应io函数的初始化函数 * 注意:在这个函数内部同时对信号进行了初始化,其实是创建了一个socketpair * 目的是将信号统一成event,见evsig_init */ base->evbase = base->evsel->init(base); } /* 如果没有找到可用的io函数,会出错返回,同时清除已经创建的base */ if (base->evbase == NULL) { event_warnx("%s: no event mechanism available", __func__); base->evsel = NULL; event_base_free(base); return NULL; } /* ... */ return (base); }
全局io函数数组,以此实现跨平台,根据预编译头判断系统是否支持某个io函数,从而构造出一个存储着所有可用的io多路复用函数的数组,初始化base时,只需要筛选出用户允许的即可
/* 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 };
接下来单独看每一个io函数的封装
select的封装,由此可见每个io函数的封装都是定义一个struct eventop类型的变量,将对应io函数的接口指针存储着,这就将所有的io函数都统一起来,不需要特定io调用特定接口
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也对每个io函数使用的数据类型进行了封装,比如说epoll_event,pollfd以及fd_set
对select的fd_set进行的封装,为什么read/write都有两份,可以参考几种服务器模型以及io多路复用函数中的select部分
struct selectop { int event_fds; /* Highest fd in fd set */ int event_fdsz; int resize_out_sets; fd_set *event_readset_in; fd_set *event_writeset_in; fd_set *event_readset_out; fd_set *event_writeset_out; };
对于epoll也是如此,libevent内部epoll有另一种封装,不明白原理
const struct eventop epollops = { "epoll", epoll_init, epoll_nochangelist_add, epoll_nochangelist_del, epoll_dispatch, epoll_dealloc, 1, /* need reinit */ EV_FEATURE_ET|EV_FEATURE_O1, 0 }; struct epollop { struct epoll_event *events; int nevents; int epfd; };
可以发现,每个io多路复用函数的封装都是遵循struct eventop类型的,所以base中只需要存储着eventop类型的指针evsel,在初始化它之后只需要调用struct eventop提供的接口函数,就可以直接调用io多路复用函数的接口函数,实现了统一
而对于每个io多路复用函数的数据类型,libevent没有进行统一的封装,因为也没有必要。在初始化base中
/* * evbase存储的就是对应的数据结构,它是个void*指针,所以可以存储任意类型的结构,比如 * 对于select而言是struct selectop, * 对于epoll而言是struct epollop * / base->evbase = base->evsel->init(base);
libevent中io多路复用的使用体现在
新建event注册到base中,此时会把监听的fd和事件添加到io复用中,本质上就是调用epoll_ctl,FD_SET等
删除event从base中,会把监听的fd从io复用中删除,本质上调用epoll_ctl等
开启事件驱动循环,监听事件的发生,本质上调用各种wait函数如epoll_wait,select,poll等
比如添加event
//函数将event添加到base的io map和io复用函数的监听事件中 int evmap_io_add(struct event_base *base, evutil_socket_t fd, struct event *ev) { /* ... */ if (evsel->add(base, ev->ev_fd, old, (ev->ev_events & EV_ET) | res, extra) == -1) return (-1); /* ... */ } //将event从io map中删除,由event_del_internal调用 int evmap_io_del(struct event_base *base, evutil_socket_t fd, struct event *ev) { /* ... */ if (evsel->del(base, ev->ev_fd, old, res, extra) == -1) return (-1); /* ... */ }
/* * 实际的事件驱动循环,其实就是一个while循环,每次调用io复用函数进行事件监听 * 监听返回之前将活跃的event都按优先级添加到base的激活队列中 * 回到循环后对base的激活队列中的event按照优先级顺序调用回调函数 * 再根据是否是永久event决定要不要从base的所有队列中删除event * 对于具有超时时间的event则需要特殊处理,见timeout_process */ int event_base_loop(struct event_base *base, int flags) { const struct eventop *evsel = base->evsel; struct timeval tv; struct timeval *tv_p; int res, done, retval = 0; /* ... */ done = 0; while (!done) { /* ... */ /* * 调用Io复用函数的监听函数,开始阻塞/非阻塞的监听 * 超时时间设置为最小堆中堆顶event的超时时间,原因如下 * * 此时监听的有三种event * 第一种是没有设置超时时间的,包括信号,所以什么时候返回都不影响 * 第二种是取得最小超时时间的堆顶event,此时可以满足在超时时间返回 * 第三种是最小堆中的其他event,这些event的超时时间在堆顶event之后,因为超时时间是绝对时间 * 也就是说如果堆顶event没有超时,那么其它的event将不可能超时 * 而当最小超时时间后返回处理超时之后重新开始监听, * 因为是绝对时间,所以不会影响最小堆的其他event的超时 * * 在返回之间,将活跃的event添加到base的激活队列中 * * 注意:不处理具有超时时间的event,因为这些event根本就没有添加到io函数中 * 处理这些是在timeout_process函数中 */ res = evsel->dispatch(base, tv_p); /* ... */ } /* ... */ return (retval); }
其实都是间接调用每一个io接口
总结
这部分主要学习到libevent是如何实现跨平台的io多路复用函数的选择的,所谓跨平台,就是将所有可能的平台都考虑到。同时看到libevent是如何把所有io函数都进行统一的,这一点很值得学习
题外话,其实对io的封装就是基类纯虚函数加各种派生类,用基类指针实现多态….
相关文章推荐
- Libevent源码分析(三)--- IO多路复用模型
- 【源码学习】python封装IO复用
- 【网络】学习IO 多路复用 select
- C/S通信---服务器IO多路复用模型之select的使用
- netty源码学习第二章:聊聊IO多路复用
- Linux IO多路复用之epoll网络编程(含源码)
- Linux IO多路复用之epoll网络编程(含源码)
- 【记录】io多路复用的学习和理解
- Libev库学习2---简单的IO多路复用服务器
- Libevent学习---evconnlistener使用和源码分析
- 使用select实现网络的多路IO复用
- muduo网络库学习(一)对io复用的封装Poller,面向对象与基于对象
- libevent学习笔记【使用篇】——7. evbuffer:缓冲IO的实用功能
- nginx源码分析之IO多路复用流程
- C/S通信---服务器IO多路复用模型之epoll的使用
- 内核源码IO多路复用之select
- Linux-(C)IO多路复用之epoll学习(转载)
- C/S通信---服务器IO多路复用模型之poll的使用
- libevent源码分析之关于IO复用的选取
- Linux-(C)IO多路复用之select学习(转载)