之前的博文讲了怎么实现线程、锁、内存分配、日志等功能的跨平台。Libevent最重要的跨平台功能还是实现了多路IO接口的跨平台(即Reactor模式)。这使得用户可以在不同的平台使用统一的接口。这篇博文就是来讲解Libevent是怎么实现这一点的。
Libevent在实现线程、内存分配、日志时,都是使用了函数指针和全局变量。在实现多路IO接口上时,Libevent也采用了这种方式,不过还是有点差别的。
相关结构体:
现在来看一下event_base结构体,下面代码只列出了本文要讲的内容:
-
- structevent_base{
- conststructeventop*evsel;
- void*evbase;
-
- …
- };
- structeventop{
- constchar*name;
-
- void*(*init)(structevent_base*);
- int(*add)(structevent_base*,evutil_socket_tfd,shortold,87); background-color:inherit; font-weight:bold">shortevents,void*fdinfo);
- int(*del)(void*fdinfo);
- int(*dispatch)(structtimeval*);
- void(*dealloc)(structevent_base*);
- intneed_reinit;
- //多路IO复用的特征。参考http://blog.csdn.net/luotuo44/article/details/38443569
- enumevent_method_featurefeatures;
- size_tfdinfo_len;
- };
可以看到event_base结构体中有一个struct eventop类型指针。而这个struct eventop结构体的成员就是一些函数指针。名称也像一个多路IO复用函数应该有的操作:add可以添加fd,del可以删除一个fd,dispatch可以进入监听。明显只要给event_base的evsel成员赋值就能使用对应的多路IO复用函数了。
选择后端:
可供选择的后端:
现在来看一下有哪些可以用的多路IO复用函数。其实在Libevent的源码目录中,已经为每一个多路IO复用函数专门创建了一个文件,如select.c、poll.c、epoll.c、kqueue.c等。
打开这些文件就可以发现在文件的前面都会声明一些多路IO复用的操作函数,而且还会定义一个struct eventop类型的全局变量。如下面代码所示:
//select.c文件
- staticvoid*select_init(staticintselect_add(int,153); background-color:inherit; font-weight:bold">void*);
- intselect_del(void*);
- intselect_dispatch(voidselect_dealloc(structeventopselectops={
- "select",
- select_init,
- select_add,
- select_del,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> select_dispatch,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> select_dealloc,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> 0,
- EV_FEATURE_FDS,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> };
-
- void*poll_init(intpoll_add(void*_idx);
- intpoll_del(void*_idx);
- intpoll_dispatch(structtimeval*);
- voidpoll_dealloc(structeventoppollops={
- "poll",
- poll_init,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> poll_add,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> poll_del,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> poll_dispatch,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> poll_dealloc,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> 0,0); background-color:inherit">/*doesn'tneed_reinit*/
- EV_FEATURE_FDS,153); background-color:inherit; font-weight:bold">sizeof(structpollidx),108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> };
如何选定后端:
看到这里,读者想必已经知道,只需将对应平台的多路IO复用函数的全局变量赋值给event_base的evsel变量即可。可是怎么让Libevent根据不同的平台选择不同的多路IO复用函数呢?另外像大部分OS都会实现select、poll和一个自己的高效多路IO复用函数。怎么从多个中选择一个呢?下面看一下Libevent的解决方案吧:
//event.c文件
- #ifdef_EVENT_HAVE_EVENT_PORTS
- externstructeventopevportops;
- #endif
- #ifdef_EVENT_HAVE_SELECT
- structeventopselectops;
- #endif
- #ifdef_EVENT_HAVE_POLL
- structeventoppollops;
- #ifdef_EVENT_HAVE_EPOLL
- structeventopepollops;
- #ifdef_EVENT_HAVE_WORKING_KQUEUE
- structeventopkqops;
- #ifdef_EVENT_HAVE_DEVPOLL
- structeventopdevpollops;
- #ifdefWIN32
- structeventopwin32ops;
- /*Arrayofbackendsinorderofpreference.*/
- structeventop*eventops[]={
- &evportops,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> #ifdef_EVENT_HAVE_WORKING_KQUEUE
- &kqops,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> #ifdef_EVENT_HAVE_EPOLL
- &epollops,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> &devpollops,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> &pollops,248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> &selectops,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> &win32ops,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> NULL
- };
它根据宏定义判断当前的OS环境是否有某个多路IO复用函数。如果有,那么就把与之对应的struct eventop结构体指针放到一个全局数组中。有了这个数组,现在只需将数组的某个元素赋值给evsel变量即可。因为是条件宏,在编译器编译代码之前完成宏的替换,所以是可以这样定义一个数组的。关于这些检测当前OS环境的宏,可以参考《event-config.h指明所在系统的环境》。
从数组的元素可以看到,低下标存的是高效多路IO复用函数。如果从低到高下标选取一个多路IO复用函数,那么将优先选择高效的。
具体实现:
现在看一下Libevent是怎么选取一个多路IO复用函数的:
structevent_base*
- event_base_new_with_config(structevent_config*cfg)
- {
- inti;
- structevent_base*base;
- intshould_check_environment;
- //分配并清零event_base内存.event_base里的所有成员都会为0
- if((base=mm_calloc(1,153); background-color:inherit; font-weight:bold">structevent_base)))==NULL){
- event_warn("%s:calloc",__func__);
- returnNULL;
- }
- ...
- should_check_environment=
- !(cfg&&(cfg->flags&EVENT_BASE_FLAG_IGNORE_ENV));
- //遍历数组的元素
- for(i=0;eventops[i]&&!base->evbase;i++){
- if(cfg!=NULL){
- /*determineifthisbackendshouldbeavoided*/
- if(event_config_is_avoided_method(cfg,108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> eventops[i]->name))
- continue;
- if((eventops[i]->features&cfg->require_features)
- !=cfg->require_features)
- continue;
- }
- /*alsoobeytheenvironmentvariables*/
- if(should_check_environment&&
- event_is_method_disabled(eventops[i]->name))
- //找到了一个满足条件的多路IO复用函数
- base->evsel=eventops[i];
- //初始化evbase,后面会说到
- base->evbase=base->evsel->init(base);
- if(base->evbase==NULL){
- event_warnx("%s:noeventmechanismavailable",248); line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> __func__);
- base->evsel=NULL;
- event_base_free(base);
- returnNULL;
- ....
- return(base);
- }
可以看到,首先从eventops数组中选出一个元素。如果设置了event_config,那么就对这个元素(即多路IO复用函数)特征进行检测,看其是否满足event_config所描述的特征。关于event_config,可以查看《多路IO复用函数的选择配置》。
后端数据存储结构体:
在本文最前面列出的event_base结构体中,除了evsel变量外,还有一个evbase变量。这也是一个很重要的变量,而且也是用于跨平台的。
像select、poll、epoll之类多路IO复用函数在调用时要传入一些数据,比如监听的文件描述符fd,监听的事件有哪些。在Libevent中,这些数据都不是保存在event_base这个结构体中的,而是存放在evbase这个指针指向的一个结构体中。
IO复用结构体:
由于不同的多路IO复用函数需要使用不同格式的数据,所以Libevent为每一个多路IO复用函数都定义了专门的结构体(即结构体是不同的),本文姑且称之为IO复用结构体。evbase指向的就是这些结构体。由于这些结构体是不同的,所以要用一个void类型指针。
在select.c、poll.c这类文件中都定义了属于自己的IO复用结构体,如下面代码所示:
structselectop{
- intevent_fds;
- intevent_fdsz;
- intresize_out_sets;
- fd_set*event_readset_in;
- fd_set*event_writeset_in;
- fd_set*event_readset_out;
- fd_set*event_writeset_out;
- structpollop{
- intevent_count;
- intnfds;
- intrealloc_copy;
- *event_set_copy*/
- structpollfd*event_set;
- structpollfd*event_set_copy;
- };
前面event_base_new_with_config的代码中,有下面一行代码:
base->evbase=base->evsel->init(base);
明显这行代码就是用来赋值evbase的。下面是poll对应的init函数:
//poll.c文件
- void*
- poll_init(structevent_base*base)
- structpollop*pollop;
- if(!(pollop=mm_calloc(1,153); background-color:inherit; font-weight:bold">structpollop))))
- return(NULL);
- evsig_init(base);
- return(pollop);
- }
经过上面的一些处理后,Libevent在特定的OS下能使用到特定的多路IO复用函数。在之前博文中说到的evmap_io_add和evmap_signal_add函数中都会调用evsel->add。由于在新建event_base时就选定了对应的多路IO复用函数,给evsel、evbase变量赋值了,所以evsel->add能把对应的fd和监听事件加到对应的IO复用结构体保存。比如poll的add函数在一开始就有下面一行代码:
structpollop*pop=base->evbase;
当然,poll的其他函数在一开始时也是会有这行代码的,因为要使用到fd和对应的监听事件等数据,就必须要获取那个IO复用结构体。
由于有evsel和evbase这个两个指针变量,当初始化完成之后,再也不用担心具体使用的多路IO复用函数是哪个了。evsel结构体的函数指针提供了统一的接口,上层的代码要使用到多路IO复用函数的一些操作函数时,直接调用evsel结构体提供的函数指针即可。也正是如此,Libevent实现了统一的跨平台Reactor接口。
之前的博文讲了怎么实现线程、锁、内存分配、日志等功能的跨平台。Libevent最重要的跨平台功能还是实现了多路IO接口的跨平台(即Reactor模式)。这使得用户可以在不同的平台使用统一的接口。这篇博文就是来讲解Libevent是怎么实现这一点的。
Libevent在实现线程、内存分配、日志时,都是使用了函数指针和全局变量。在实现多路IO接口上时,Libevent也采用了这种方式,不过还是有点差别的。
structpollop*pop=base->evbase;
当然,poll的其他函数在一开始时也是会有这行代码的,因为要使用到fd和对应的监听事件等数据,就必须要获取那个IO复用结构体。
由于有evsel和evbase这个两个指针变量,当初始化完成之后,再也不用担心具体使用的多路IO复用函数是哪个了。evsel结构体的函数指针提供了统一的接口,上层的代码要使用到多路IO复用函数的一些操作函数时,直接调用evsel结构体提供的函数指针即可。也正是如此,Libevent实现了统一的跨平台Reactor接口。
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|