Libevent原始碼分析-----配置event_base
前面的博文都是講一些Libevent的一些輔助結構,現在來講一下關鍵結構體:event_base。
這裡作一個提醒,在閱讀Libevent原始碼時,會經常看到backend這個單詞。其直譯是“後端”。實際上其指的是Libevent內部使用的多路IO複用函式,多路IO複用函式就是select、poll、epoll這類函式。本系列博文中,為了敘述方便,“多路IO複用函式”與“後端”這兩種說法都會採用。
配置結構體:
通常我們獲取event_base都是通過event_base_new()這個無參函式。使用這個無參函式,只能得到一個預設配置的event_base結構體。本文主要是講一些怎麼獲取一個非預設配置的event_base以及可以對event_base進行哪些配置。
還是先看一下event_base_new函式吧。
//event.c檔案
struct event_base *
event_base_new(void)
{
struct event_base *base = NULL;
struct event_config *cfg = event_config_new();
if (cfg) {
base = event_base_new_with_config(cfg);
event_config_free(cfg);
}
return base;
}
可以看到,其先建立了一個event_config結構體,並用cfg指標指向之,然後再用這個變數作為引數呼叫event_base_new_with_config。因為並沒有對cfg進行任何的設定,所以得到的是預設配置的event_base。
從這裡也可以知道,如果要對event_base進行配置,那麼對cfg變數進行配置即可。現在我們的目光從event_base結構體轉到event_config結構體。
先來看看event_config結構體的定義。
struct event_config { TAILQ_HEAD(event_configq, event_config_entry) entries; int n_cpus_hint; enum event_method_feature require_features; enum event_base_config_flag flags; }; struct event_config_entry { TAILQ_ENTRY(event_config_entry) next; const char *avoid_method; };
我們要做的就是對event_config結構體的那四個成員變數進行配置。
具體的配置內容:
拒絕使用某個後端:
第一個成員entries,其結構就不展開了,關於TAILQ_HEAD,可以參考《TAILQ_QUEUE佇列》。這裡知道它是表示一個佇列即可,佇列元素的型別就是event_config_entry,可以用來儲存一個字串指標。它對應的設定函式為event_config_avoid_method。
Libevent是跨平臺的Reactor,對於事件監聽,其內部是使用多路IO複用函式。比較通用的多路IO複用函式是select和poll。而很多平臺都提出了自己的高效多路IO複用函式,比如:epoll、devpoll、kqueue。Libevent對於這些多路IO複用函式都進行包裝,供自己使用。event_config_avoid_method函式就是指出,避免使用指定的多路IO複用函式。其是通過字串的方式指定的,即引數method。這個字串將由佇列元素event_config_entry的avoid_method成員變數儲存(由於是指標,所以更準確來說是指向)。
檢視Libevent原始碼包裡的檔案,可以發現有諸如epoll.c、select.c、poll.c、devpoll.c、kqueue.c這些檔案。開啟這些檔案就可以發現在檔案內容的前面都會定義一個struct eventop型別變數。該結構體的第一個成員必然是一個字串。這個字串就描述了對應的多路IO複用函式的名稱。所以是可以通過名稱來禁用某種多路IO複用函式的。
下面是event_config_avoid_method函式的實現。其作用是把method指明的各個名稱記錄到entries成員變數中。
int
event_config_avoid_method(struct event_config *cfg, const char *method)
{
struct event_config_entry *entry = mm_malloc(sizeof(*entry));
if (entry == NULL)
return (-1);
//複製字串
if ((entry->avoid_method = mm_strdup(method)) == NULL) {
mm_free(entry);
return (-1);
}
//插入到佇列中
TAILQ_INSERT_TAIL(&cfg->entries, entry, next);
return (0);
}
上面的程式碼是設定拒絕使用某一個多路IO複用函式,在建立一個event_base時怎麼進行選擇的可以參考這一個連結。
智慧調整CPU個數:
第二個成員變數n_cpus_hint。從名字來看是指明CPU的數量。是通過函式event_config_set_num_cpus_hint來設定的。其作用是告訴event_config,系統中有多少個CPU,以便作一些對執行緒池作一些調整來獲取更高的效率。目前,僅僅Window系統的IOCP(Windows的IOCP能夠根據CPU的個數智慧調整),該函式的設定才有用。在以後,Libevent可能會將之應用於其他系統。
正如其名字中的hint,這僅僅是一個提示。就如同C++中的inline。event_base實際使用的CPU個數不一定等於提示的個數。
規定所選後端需滿足的特徵:
第三個成員變數require_features。從其名稱來看是要求的特徵。不錯,這個變數指定 多路IO複用函式應該滿足哪些特徵。所有的特徵定義在一個列舉型別中。
//event.h檔案
enum event_method_feature {
//支援邊沿觸發
EV_FEATURE_ET = 0x01,
//新增、刪除、或者確定哪個事件啟用這些動作的時間複雜度都為O(1)
//select、poll是不能滿足這個特徵的.epoll則滿足
EV_FEATURE_O1 = 0x02,
//支援任意的檔案描述符,而不能僅僅支援套接字
EV_FEATURE_FDS = 0x04
};
這個成員變數是通過event_config_require_features函式設定的。該函式的內部還是挺簡單的。
int
event_config_require_features(struct event_config *cfg,
int features)
{
if (!cfg)
return (-1);
cfg->require_features = features;
return (0);
}
從函式的實現可以看到,如果要設定多個特徵,不能呼叫該函式多次,而應該使用位操作。比如: EV_FEATURE_O1 | EV_FEATURE_FDS作為引數。
值得注意的是,對於某些系統,可能其提供的多路IO複用函式不能滿足event_config_require_features函式要求的特徵,此時event_base_new_with_config函式將返回NULL,即得不到一個滿足條件的event_base。所以在設定這個特徵時,那麼就要檢查event_base_new_with_config的返回值是否為NULL,像下面程式碼那樣。
#include<event.h>
#include<stdio.h>
int main()
{
event_config *cfg = event_config_new();
event_config_require_features(cfg, EV_FEATURE_O1 | EV_FEATURE_FDS);
event_base *base = event_base_new_with_config(cfg);
if( base == NULL )
{
printf("don't support this features\n");
base = event_base_new(); //使用預設的。
}
…..
return 0;
}
上面程式碼中,如果是在Linux執行,也是返回NULL。即epoll都不能同時滿足那個兩個特徵。
那麼怎麼知道多路IO複用函式支援哪些特徵呢?前面說到的一個機構體struct eventop中有一個成員正是enum event_method_feature features。在Libevent-2.0.21-stable中是倒數第二個成員。開啟epoll.c、select.c、poll.c、devpoll.c、kqueue.c這些檔案,檢視裡面定義的struct eventop型別變數,就可以看到各個多路IO複用函式都支援哪些特徵。在epoll.c檔案可以看到,epoll支援EV_FEATURE_ET|EV_FEATURE_O1。所以前面的程式碼中,返回NULL。
其他一些設定:
第四個變數flags是通過函式event_config_set_flag設定的。函式的實現很簡單。注意,函式的內部是進行 |= 運算的。
//event.c檔案
int
event_config_set_flag(struct event_config *cfg, int flag)
{
if (!cfg)
return -1;
cfg->flags |= flag;
return 0;
}
現在來看一下引數flag可以取哪些值。
- EVENT_BASE_FLAG_NOLOCK:不要為event_base分配鎖。設定這個選項可以為event_base節省一點加鎖和解鎖的時間,但是當多個執行緒訪問event_base會變得不安全
- EVENT_BASE_FLAG_IGNORE_ENV:選擇多路IO複用函式時,不檢測EVENT_*環境變數。使用這個標誌要考慮清楚:因為這會使得使用者更難除錯程式與Libevent之間的互動
- EVENT_BASE_FLAG_STARTUP_IOCP:僅用於Windows。這使得Libevent在啟動時就啟用任何必需的IOCP分發邏輯,而不是按需啟用。如果設定了這個巨集,那麼evconn_listener_new和bufferevent_socket_new函式的內部將使用IOCP
- EVENT_BASE_FLAG_NO_CACHE_TIME:在執行event_base_loop的時候沒有cache時間。該函式的while迴圈會經常取系統時間,如果cache時間,那麼就取cache的。如果沒有的話,就只能通過系統提供的函式來獲取系統時間。這將更耗時
- EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST:告知Libevent,如果決定使用epoll這個多路IO複用函式,可以安全地使用更快的基於changelist 的多路IO複用函式:epoll-changelist多路IO複用可以在多路IO複用函式呼叫之間,同樣的fd 多次修改其狀態的情況下,避免不必要的系統呼叫。但是如果傳遞任何使用dup()或者其變體克隆的fd給Libevent,epoll-changelist多路IO複用函式會觸發一個核心bug,導致不正確的結果。在不使用epoll這個多路IO複用函式的情況下,這個標誌是沒有效果的。也可以通過設定EVENT_EPOLL_USE_CHANGELIST 環境變數來開啟epoll-changelist選項
綜觀上面4個變數的設定,特徵設定event_config_require_features和CPU數目設定event_config_set_num_cpus_hint兩者的函式呼叫會覆蓋之前的設定。如果要同時設定多個,那麼需要在引數中使用位運算中的 | 。而另外兩個變數的設定可以通過多次呼叫函式的方式同時設定多個值。
獲取當前配置:
前面的介紹的都是設定,現在來講一下獲取。主要有下面幾個。
const char **event_get_supported_methods(void);
const char *event_base_get_method(const struct event_base *);
int event_base_get_features(const struct event_base *base);
static int event_config_is_avoided_method(const struct event_config *cfg, const char *method)
第一個函式是獲取當前系統所支援的多路IO複用函式有哪些。第二個函式需要一個event_base結構體作為引數,說明是在new到一個event_base之後才能呼叫的。該函式返回值是對應event_base* 當前所採用的多路IO複用函式是哪個。第三個函式則是獲取引數event_base當前所採用的特徵是什麼。第四個函式則說明引數method指明的多路IO複用函式是不是被引數cfg所禁用了。如果是禁用了,返回非0值。不禁用就返回0。
#include<event2/event.h>
#include<stdio.h>
#ifdef WIN32
#include<WinSock2.h>
#endif
int main()
{
#ifdef WIN32
WSADATA wsa_data;
WSAStartup(0x0201, &wsa_data);
#endif
const char** all_methods = event_get_supported_methods();
while( all_methods && *all_methods )
{
printf("%s\t", *all_methods++);
}
printf("\n");
event_base *base = event_base_new();
if( base )
printf("current method:\t %s\n", event_base_get_method(base) );
else
printf("base == NULL\n");
#ifdef WIN32
WSACleanup();
#endif
return 0;
}
上面程式碼在Ubuntu10.04上執行,其結果為:
epoll poll select
currentmethod: epoll
在Win7 + VS2010的執行結果為
win32
currentmethod: win32
參考: