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

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

 

 

相关结构体:

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

//event-internal.h文件

struct event_base {

const struct eventop *evsel;

void *evbase;


…

};

struct eventop {

const char *name; //多路IO复用函数的名字


void *(*init)(struct event_base *);


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 *);

void (*dealloc)(struct event_base *);


int need_reinit; //是否要重新初始化

//多路IO复用的特征。参考http://blog.csdn.net/luotuo44/article/details/38443569

enum event_method_feature features;

size_t fdinfo_len; //额外信息的长度。有些多路IO复用函数需要额外的信息

};

        可以看到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文件

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,

};


//poll.c文件

static void *poll_init(struct event_base *);

static int poll_add(struct event_base *, int, short old, short events, void *_idx);

static int poll_del(struct event_base *, int, short old, short events, void *_idx);

static int poll_dispatch(struct event_base *, struct timeval *);

static void poll_dealloc(struct event_base *);


const struct eventop pollops = {

"poll",

poll_init,

poll_add,

poll_del,

poll_dispatch,

poll_dealloc,

0, /* doesn't need_reinit */

EV_FEATURE_FDS,

sizeof(struct pollidx),

};


 

       

如何选定后端:

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

//event.c文件

#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

&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

};

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

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

 

具体实现:

        现在看一下Libevent是怎么选取一个多路IO复用函数的:

//event.c文件

struct event_base *

event_base_new_with_config(const struct event_config *cfg)

{

int i;

struct event_base *base;

int should_check_environment;


//分配并清零event_base内存. event_base里的所有成员都会为0

if ((base = mm_calloc(1, sizeof(struct event_base))) == NULL) {

event_warn("%s: calloc", __func__);

return NULL;

}


...

should_check_environment =

!(cfg && (cfg->flags & EVENT_BASE_FLAG_IGNORE_ENV));

//遍历数组的元素

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;


//找到了一个满足条件的多路IO复用函数

base->evsel = eventops[i];


//初始化evbase,后面会说到

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;

}


....


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复用结构体,如下面代码所示:

//select.c文件

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;

};


//poll.c文件

struct pollop {

int event_count; /* Highest number alloc */

int nfds; /* Highest number used */

int realloc_copy; /* True iff we must realloc

* event_set_copy */

struct pollfd *event_set;

struct pollfd *event_set_copy;

};


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

 

base->evbase = base->evsel->init(base);

        明显这行代码就是用来赋值evbase的。下面是poll对应的init函数:

//poll.c文件

static void *

poll_init(struct event_base *base)

{

struct pollop *pollop;


if (!(pollop = mm_calloc(1, sizeof(struct pollop))))

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函数在一开始就有下面一行代码:

 

struct pollop*pop = base->evbase;

 

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

        由于有evsel和evbase这个两个指针变量,当初始化完成之后,再也不用担心具体使用的多路IO复用函数是哪个了。evsel结构体的函数指针提供了统一的接口,上层的代码要使用到多路IO复用函数的一些操作函数时,直接调用evsel结构体提供的函数指针即可。也正是如此,Libevent实现了统一的跨平台Reactor接口。 --------------------- 本文来自 luotuo44 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/luotuo44/article/details/38458469?utm_source=copy

10-04 12:25