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