這兩天大致看了看
libevent的代碼,簡單做一個分析.
libevent最大的特點就是封裝了對以下三種事件的響應:IO事件,定時器事件,信號事件.這里就分析libevent如果做到這一點的,在libevent中還包括一些其他的功能(如緩沖區),但是我這里就重點講解這一部分了.
事件原型,簡單看一看用于封裝事件的結構體定義:
struct event {
TAILQ_ENTRY (event) ev_next;
TAILQ_ENTRY (event) ev_active_next;
TAILQ_ENTRY (event) ev_signal_next;
unsigned int min_heap_idx; /* for managing timeouts */
struct event_base *ev_base;
int ev_fd;
short ev_events;
short ev_ncalls;
short *ev_pncalls; /* Allows deletes in callback */
struct timeval ev_timeout;
int ev_pri; /* smaller numbers are higher priority */
void (*ev_callback)(int, short, void *arg);
void *ev_arg;
int ev_res; /* result passed to event callback */
int ev_flags;
};
其中的ev_callback就是回調函數,也就是說當所關注的事件發生時所要觸發的函數是注冊到這個函數指針中的.
1)IO事件:再簡單不過了,對select/epoll/poll等之類的調用進行封裝即可,所提供的接口無非這幾種:
struct eventop {
const char *name;
void *(*init)(struct event_base *);
int (*add)(void *, struct event *);
int (*del)(void *, struct event *);
int (*dispatch)(struct event_base *, void *, struct timeval *);
void (*dealloc)(struct event_base *, void *);
/* set if we need to reinitialize the event base */
int need_reinit;
};
在我看過的很多開源服務器源碼(如lighttpd)中都有類似的封裝,不是什么新鮮的東西.
2)定時器事件:libevent采用堆數據結構存放所要定時的事件的時間,大家知道堆可以用來實現優先隊列,在這里,所有的定時器就放在這樣的一個數據結構中了.
3)信號事件:所有的信號都注冊回調函數為evsignal_handler(在signal.c中),這個函數的功能就是在某信號被觸發的時候將該信號被觸發的計數器加1,同時置一個標志位表示有信號被觸發.
現在,把所有這些結合起來,看看libevent框架的主循環是如何工作的,用簡單的偽碼表示:
主循環
更新當前時間
將當前時間與存放時間的堆中的時間依次進行比較,由于是采用堆實現的,這里查找相當的快,于是所有可以被觸發的定時器事件都從堆中被取出,同時取下的事件被放到一個活動事件的隊列中
調用封裝IO操作的dispatch函數,在其中也將被觸發的IO事件加入到那個存放活動事件的隊列中
在dispatch的函數中如果信號被觸發的標志位被置位,說明有信號被觸發,調用evsignal_process函數,這個函數的功能也是把所有被觸發的事件放到活動事件的隊列中
好了,現在所有可以被觸發的事件都在活動事件隊列中了,依次遍歷取出來調用它們注冊的回調函數就成了.
上面就是libevent處理這三種事件的大體框架.
說一說我認為這個框架存在的缺點:
1) callback函數只能有一個,假設這樣一個場景,我需要對某個連接socket同時監控它的可讀/可寫/超時事件,那么我需要針對同一個socket fd生成三個event對象.
2) 在主循環中,每次都要去查詢存放時間的堆看看有沒有定時器事件可以被觸發,問題在于,很多時候,一個主循環很快就到了下一次,而時間過去的并不多,這次去檢查時間是冗余的操作,當然了,由于libevent的定時器是精確到毫秒級別的,所以有這么做的必要,但是在一個真正的服務器中,我懷疑有多少需要精確到微秒級別的事件,所以呢,我覺得這個可以做一個改進,每次更新時間之后跟上一次更新的時間做一個比較,如果超過了一秒(或者把這個間隔改成可以由使用者配置的)再去檢查堆上面的時間.