1. 程式人生 > >Libevent原始碼分析-----跨平臺Reactor介面的實現

Libevent原始碼分析-----跨平臺Reactor介面的實現

        之前的博文講了怎麼實現執行緒、鎖、記憶體分配、日誌等功能的跨平臺。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