Libevent源码分析-----跨平台Reactor接口的实现

出处: http://blog.csdn.net/luotuo44/article/details/38458469



        之前的博文讲了怎么实现线程、锁、内存分配、日志等功能的跨平台。Libevent最重要的跨平台功能还是实现了多路IO接口的跨平台(即Reactor模式)。这使得用户可以在不同的平台使用统一的接口。这篇博文就是来讲解Libevent是怎么实现这一点的。

        Libevent在实现线程、内存分配、日志时,都是使用了函数指针和全局变量。在实现多路IO接口上时,Libevent也采用了这种方式,不过还是有点差别的。


相关结构体:

        现在来看一下event_base结构体,下面代码只列出了本文要讲的内容:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //event-internal.h文件  
  2. struct event_base {  
  3.     const struct eventop *evsel;  
  4.     void *evbase;  
  5.   
  6.     …  
  7. };  
  8.     struct eventop {  
  9.     const char *name; //多路IO复用函数的名字  
  10.   
  11.     void *(*init)(struct event_base *);  
  12.   
  13.     int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);  
  14.     int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);  
  15.     int (*dispatch)(struct event_base *, struct timeval *);  
  16.     void (*dealloc)(struct event_base *);  
  17.   
  18.     int need_reinit; //是否要重新初始化  
  19.     //多路IO复用的特征。参考http://blog.csdn.net/luotuo44/article/details/38443569  
  20.     enum event_method_feature features;  
  21.     size_t fdinfo_len; //额外信息的长度。有些多路IO复用函数需要额外的信息  
  22. };  

        可以看到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类型的全局变量。如下面代码所示:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //select.c文件  
  2. static void *select_init(struct event_base *);  
  3. static int select_add(struct event_base *, intshort old, short events, void*);  
  4. static int select_del(struct event_base *, intshort old, short events, void*);  
  5. static int select_dispatch(struct event_base *, struct timeval *);  
  6. static void select_dealloc(struct event_base *);  
  7.   
  8. const struct eventop selectops = {  
  9.     "select",  
  10.     select_init,  
  11.     select_add,  
  12.     select_del,  
  13.     select_dispatch,  
  14.     select_dealloc,  
  15.     0, /* doesn't need reinit. */  
  16.     EV_FEATURE_FDS,  
  17.     0,  
  18. };  
  19.   
  20. //poll.c文件  
  21. static void *poll_init(struct event_base *);  
  22. static int poll_add(struct event_base *, intshort old, short events, void *_idx);  
  23. static int poll_del(struct event_base *, intshort old, short events, void *_idx);  
  24. static int poll_dispatch(struct event_base *, struct timeval *);  
  25. static void poll_dealloc(struct event_base *);  
  26.   
  27. const struct eventop pollops = {  
  28.     "poll",  
  29.     poll_init,  
  30.     poll_add,  
  31.     poll_del,  
  32.     poll_dispatch,  
  33.     poll_dealloc,  
  34.     0, /* doesn't need_reinit */  
  35.     EV_FEATURE_FDS,  
  36.     sizeof(struct pollidx),  
  37. };  

       

如何选定后端:

        看到这里,读者想必已经知道,只需将对应平台的多路IO复用函数的全局变量赋值给event_base的evsel变量即可。可是怎么让Libevent根据不同的平台选择不同的多路IO复用函数呢?另外像大部分OS都会实现select、poll和一个自己的高效多路IO复用函数。怎么从多个中选择一个呢?下面看一下Libevent的解决方案吧:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //event.c文件  
  2. #ifdef _EVENT_HAVE_EVENT_PORTS  
  3. extern const struct eventop evportops;  
  4. #endif  
  5. #ifdef _EVENT_HAVE_SELECT  
  6. extern const struct eventop selectops;  
  7. #endif  
  8. #ifdef _EVENT_HAVE_POLL  
  9. extern const struct eventop pollops;  
  10. #endif  
  11. #ifdef _EVENT_HAVE_EPOLL  
  12. extern const struct eventop epollops;  
  13. #endif  
  14. #ifdef _EVENT_HAVE_WORKING_KQUEUE  
  15. extern const struct eventop kqops;  
  16. #endif  
  17. #ifdef _EVENT_HAVE_DEVPOLL  
  18. extern const struct eventop devpollops;  
  19. #endif  
  20. #ifdef WIN32  
  21. extern const struct eventop win32ops;  
  22. #endif  
  23.   
  24. /* Array of backends in order of preference. */  
  25. static const struct eventop *eventops[] = {  
  26. #ifdef _EVENT_HAVE_EVENT_PORTS  
  27.     &evportops,  
  28. #endif  
  29. #ifdef _EVENT_HAVE_WORKING_KQUEUE  
  30.     &kqops,  
  31. #endif  
  32. #ifdef _EVENT_HAVE_EPOLL  
  33.     &epollops,  
  34. #endif  
  35. #ifdef _EVENT_HAVE_DEVPOLL  
  36.     &devpollops,  
  37. #endif  
  38. #ifdef _EVENT_HAVE_POLL  
  39.     &pollops,  
  40. #endif  
  41. #ifdef _EVENT_HAVE_SELECT  
  42.     &selectops,  
  43. #endif  
  44. #ifdef WIN32  
  45.     &win32ops,  
  46. #endif  
  47.     NULL  
  48. };  

        它根据宏定义判断当前的OS环境是否有某个多路IO复用函数。如果有,那么就把与之对应的struct eventop结构体指针放到一个全局数组中。有了这个数组,现在只需将数组的某个元素赋值给evsel变量即可。因为是条件宏,在编译器编译代码之前完成宏的替换,所以是可以这样定义一个数组的。关于这些检测当前OS环境的宏,可以参考《event-config.h指明所在系统的环境》。

        从数组的元素可以看到,低下标存的是高效多路IO复用函数。如果从低到高下标选取一个多路IO复用函数,那么将优先选择高效的。


具体实现:

        现在看一下Libevent是怎么选取一个多路IO复用函数的:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //event.c文件  
  2. struct event_base *  
  3. event_base_new_with_config(const struct event_config *cfg)  
  4. {  
  5.     int i;  
  6.     struct event_base *base;  
  7.     int should_check_environment;  
  8.   
  9.     //分配并清零event_base内存. event_base里的所有成员都会为0  
  10.     if ((base = mm_calloc(1, sizeof(struct event_base))) == NULL) {  
  11.         event_warn("%s: calloc", __func__);  
  12.         return NULL;  
  13.     }     
  14.   
  15.     ...  
  16.     should_check_environment =  
  17.         !(cfg && (cfg->flags & EVENT_BASE_FLAG_IGNORE_ENV));   
  18.         //遍历数组的元素  
  19.     for (i = 0; eventops[i] && !base->evbase; i++) {  
  20.         if (cfg != NULL) {  
  21.             /* determine if this backend should be avoided */  
  22.             if (event_config_is_avoided_method(cfg,  
  23.                 eventops[i]->name))  
  24.                 continue;  
  25.             if ((eventops[i]->features & cfg->require_features)  
  26.                 != cfg->require_features)  
  27.                 continue;  
  28.         }  
  29.   
  30.         /* also obey the environment variables */  
  31.         if (should_check_environment &&  
  32.             event_is_method_disabled(eventops[i]->name))  
  33.             continue;  
  34.   
  35.         //找到了一个满足条件的多路IO复用函数  
  36.         base->evsel = eventops[i];  
  37.   
  38.         //初始化evbase,后面会说到  
  39.         base->evbase = base->evsel->init(base);  
  40.     }  
  41.   
  42.     if (base->evbase == NULL) {  
  43.         event_warnx("%s: no event mechanism available",  
  44.             __func__);  
  45.         base->evsel = NULL;  
  46.         event_base_free(base);  
  47.         return NULL;  
  48.     }  
  49.   
  50.     ....  
  51.   
  52.     return (base);  
  53. }  

        可以看到,首先从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复用结构体,如下面代码所示:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //select.c文件  
  2. struct selectop {  
  3.     int event_fds;      /* Highest fd in fd set */  
  4.     int event_fdsz;  
  5.     int resize_out_sets;  
  6.     fd_set *event_readset_in;  
  7.     fd_set *event_writeset_in;  
  8.     fd_set *event_readset_out;  
  9.     fd_set *event_writeset_out;  
  10. };  
  11.   
  12. //poll.c文件  
  13. struct pollop {  
  14.     int event_count;        /* Highest number alloc */  
  15.     int nfds;           /* Highest number used */  
  16.     int realloc_copy;       /* True iff we must realloc 
  17.                      * event_set_copy */  
  18.     struct pollfd *event_set;  
  19.     struct pollfd *event_set_copy;  
  20. };  

        前面event_base_new_with_config的代码中,有下面一行代码:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. base->evbase = base->evsel->init(base);  
        明显这行代码就是用来赋值evbase的。下面是poll对应的init函数:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //poll.c文件  
  2. static void *  
  3. poll_init(struct event_base *base)  
  4. {  
  5.     struct pollop *pollop;  
  6.   
  7.     if (!(pollop = mm_calloc(1, sizeof(struct pollop))))  
  8.         return (NULL);  
  9.   
  10.     evsig_init(base);//其他的一些初始化  
  11.   
  12.     return (pollop);  
  13. }  

        经过上面的一些处理后,Libevent在特定的OS下能使用到特定的多路IO复用函数。在之前博文中说到的evmap_io_add和evmap_signal_add函数中都会调用evsel->add。由于在新建event_base时就选定了对应的多路IO复用函数,给evsel、evbase变量赋值了,所以evsel->add能把对应的fd和监听事件加到对应的IO复用结构体保存。比如poll的add函数在一开始就有下面一行代码:

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. struct pollop*pop = base->evbase;  

        当然,poll的其他函数在一开始时也是会有这行代码的,因为要使用到fd和对应的监听事件等数据,就必须要获取那个IO复用结构体。

        由于有evsel和evbase这个两个指针变量,当初始化完成之后,再也不用担心具体使用的多路IO复用函数是哪个了。evsel结构体的函数指针提供了统一的接口,上层的代码要使用到多路IO复用函数的一些操作函数时,直接调用evsel结构体提供的函数指针即可。也正是如此,Libevent实现了统一的跨平台Reactor接口。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
libevent是一个事件触发的网络库,适用于windows、linux、bsd等多种平台,内部使用select、epoll、kqueue等系统调用管理事件机制。著名分布式缓存软件memcached也是libevent based,而且libevent在使用上可以做到跨平台,而且根据libevent官方网站上公布的数据统计,似乎也有着非凡的性能。 编辑本段 详细   编译库代码,编译脚本会判断OS支持哪种类型的事件机制(select、epoll或kqueue),然后条件编译相应代码,供上层使用的接口仍然是保持统一的(否则也不能所谓的跨平台了)。在linux redhat as 4 u 2 上编译相当容易,configure以后make,make install就可以了,windows上编译似乎有点小麻烦,不过稍微改点东西也就通过了。   从代码中看,libevent支持用户使用三种类型的事件,分别是网络IO、定时器、信号三种,在定时器的实现上使用了RB tree的数据结构,以达到高效查找、排序、删除定时器的目的,网络IO上,主要关注了一下linux上的epoll(因为目前的开发主要在linux平台),结果发现libevent的epoll居然用的EPOLLLT,水平触发的方式用起来比较方便,不容易出错,但是在效率上可能比EPOLLET要低一些。   跟网络无关的,libevent也有一些缓冲区管理的函数,而且是c风格的函数,实用性不是太大。libevent没有提供缓存的函数。   虽然libevent实用上的价值不大,但它提供的接口形式还是不错的,实现类似的lib的时候仍然是可以参考的。   Libevent定时器的数据结构自version 1.4起已由红黑树改为最小堆(Min Heap),以提高效率;网络IO和信号的数据结构采用了双向链表(TAILQ)。在实现上主要有3种链表: EVLIST_INSERTED, EVLIST_ACTIVE, EVLIST_TIMEOUT,一个ev在这3种链表之间被插入或删除,处于EVLIST_ACTIVE链表中的ev最后将会被调度执行。   Libevent提供了DNS,HTTP Server,RPC等组件,HTTP Server可以说是Libevent的经典应用。从http.c可看到Libevent的很多标准写法。写非阻塞式的HTTP Server很容易将socket处理与HTTP协议处理纠缠在一起,Libevent在这点上似乎也有值得推敲的地方。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值