Libevent原始碼分析-----Libevent時間管理
基本時間操作函式:
Libevent採用的時間型別是struct timeval,這個型別在很多平臺都提供了。此外,Libevent還提供了一系列的時間操作函式。比如兩個struct timeval相加、相減、比較大小。有些平臺直接提供了一些時間操作函式,但有些則沒有,那麼Libevent就自己實現。這些巨集如下:
程式碼中的那些條件巨集,是在配置Libevent的時候檢查所在的系統環境而定義的。具體的內容,可以參考《event-config.h指明所在系統的環境》一文。#ifdef _EVENT_HAVE_TIMERADD #define evutil_timeradd(tvp, uvp, vvp) timeradd((tvp), (uvp), (vvp)) #define evutil_timersub(tvp, uvp, vvp) timersub((tvp), (uvp), (vvp)) #else #define evutil_timeradd(tvp, uvp, vvp) \ do { \ (vvp)->tv_sec = (tvp)->tv_sec + (uvp)->tv_sec; \ (vvp)->tv_usec = (tvp)->tv_usec + (uvp)->tv_usec; \ if ((vvp)->tv_usec >= 1000000) { \ (vvp)->tv_sec++; \ (vvp)->tv_usec -= 1000000; \ } \ } while (0) #define evutil_timersub(tvp, uvp, vvp) \ do { \ (vvp)->tv_sec = (tvp)->tv_sec - (uvp)->tv_sec; \ (vvp)->tv_usec = (tvp)->tv_usec - (uvp)->tv_usec; \ if ((vvp)->tv_usec < 0) { \ (vvp)->tv_sec--; \ (vvp)->tv_usec += 1000000; \ } \ } while (0) #endif #ifdef _EVENT_HAVE_TIMERCLEAR #define evutil_timerclear(tvp) timerclear(tvp) #else #define evutil_timerclear(tvp) (tvp)->tv_sec = (tvp)->tv_usec = 0 #endif #define evutil_timercmp(tvp, uvp, cmp) \ (((tvp)->tv_sec == (uvp)->tv_sec) ? \ ((tvp)->tv_usec cmp (uvp)->tv_usec) : \ ((tvp)->tv_sec cmp (uvp)->tv_sec)) #ifdef _EVENT_HAVE_TIMERISSET #define evutil_timerisset(tvp) timerisset(tvp) #else #define evutil_timerisset(tvp) ((tvp)->tv_sec || (tvp)->tv_usec) #endif
Libevent的時間一般是用在超時event的。對於超時event,使用者只需給出一個超時時間,比如多少秒,而不是一個絕對時間。但在Libevent內部,要將這個時間轉換成絕對時間。所以在Libevent內部會經常獲取系統時間(絕對時間),然後進行一些處理,比如,轉換、比較。
cache時間:
Libevent封裝了一個evutil_gettimeofday函式用來獲取系統時間,該函式在POSIX的系統是直接呼叫gettimeofday函式,在Windows系統是通過_ftime函式。雖然gettimeofday的//event.c檔案 static int gettime(struct event_base *base, struct timeval *tp) { if (base->tv_cache.tv_sec) { //cache可用 *tp = base->tv_cache; return (0); } …//沒有cache的時候就使用其他方式獲取時間 }
從上面程式碼可以看到,Libevent優先使用cache時間。tv_bache變數處理作為cache外,還有另外一個作用,下面會講到。
cache的時間也是通過呼叫系統的提供的時間函式得到的。
//event.c檔案
static inline void
update_time_cache(struct event_base *base)
{
base->tv_cache.tv_sec = 0;
if (!(base->flags & EVENT_BASE_FLAG_NO_CACHE_TIME))
gettime(base, &base->tv_cache);
}
tv_cache是通過呼叫gettime來獲取時間。由於tv_cache.tv_sec已經賦值為0,所以它將使用系統提供的時間函式得到時間。程式碼也展示了,如果event_base的配置中定義了EVENT_BASE_FLAG_NO_CACHE_TIME巨集,將不能使用cache時間。關於這個巨集的設定可以參考《配置event_base》一文。
處理使用者手動修改系統時間:
如果使用者能老老實實,或許程式碼就不需要寫得很複雜。由於使用者的不老實,所以有時候要考慮很多很特殊的情況。在Libevent的時間管理這方面也是如此。
Libevent在實際使用時還有一個坑爹的現象,那就是,使用者手動把時鐘(wall time)往回調了。比如說現在是上午9點,但使用者卻把OS的系統時間調成了上午7點。這是很坑爹的。對於超時event和event_add的第二個引數,都是一個時間長度。但在內部Libevent要把這個時間轉換成絕對時間。
如果使用者手動修改了OS的系統時間。那麼Libevent把超時時間長度轉換成絕對時間將是弄巧成拙。拿上面的時間例子。如果使用者設定的超時為1分鐘。那麼到了9:01就會超時。如果使用者把系統時間調成了7點,那麼要過2個小時01分才能發生超時。這就和使用者原先的設定差得很遠了。
讀者可能會說,這個責任應該是由使用者負。呵呵,但Libevent提供的函式介面是一個時間長度,既然是時間長度,那麼無論使用者怎麼改變OS的系統時間,這個時間長度都是相對於event_add ()被呼叫的那一刻算起,這是不會變的。如果Libevent做不到這一點,這說明是Libevent沒有遵循介面要求。
為此,Libevent提出了一些解決方案。
使用monotonic時間:
問題的由來是因為使用者能修改系統時間,所以最簡單的解決方案就是能獲取到一個使用者不能修改的時間,然後以之為絕對時間。因為event_add提供給使用者的介面使用的是一個時間長度,所以無論是使用哪個絕對時間都是無所謂的。
基於這一點,Libevent找到了monotonic時間,從字面來看monotonic翻譯成單調。我們高中學過的單調函式英文名就是monotonic function。monotonic時間就像單調遞增函式那樣,只增不減的,沒有人能手動修改之。
monotonic時間是boot啟動後到現在的時間。使用者是不能修改這個時間。如果Libevent所在的系統支援monotonic時間的話,那麼Libevent就會選用這個monotonic時間為絕對時間。
首先,Libevent檢查所在的系統是否支援monotonic時間。在event_base_new_with_config函式中會呼叫detect_monotonic函式檢測。
//event.c檔案
static void
detect_monotonic(void)
{
#if defined(_EVENT_HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
struct timespec ts;
static int use_monotonic_initialized = 0;
if (use_monotonic_initialized)
return;
if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
use_monotonic = 1; //系統支援monotonic時間
use_monotonic_initialized = 1;
#endif
}
從上面程式碼可以看到,如果Libevent所在的系統支援monotonic時間,就將全域性變數use_monotonic賦值1,作為標誌。
如果Libevent所在的系統支援monotonic時間,那麼Libevent將使用monotonic時間,也就是說Libevent用於獲取系統時間的函式gettime將由monotonic提供時間。//event.c檔案
static int
gettime(struct event_base *base, struct timeval *tp)
{
EVENT_BASE_ASSERT_LOCKED(base);
if (base->tv_cache.tv_sec) {
*tp = base->tv_cache;
return (0);
}
#if defined(_EVENT_HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
if (use_monotonic) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1)
return (-1);
tp->tv_sec = ts.tv_sec;
tp->tv_usec = ts.tv_nsec / 1000;
//額外的功能
if (base->last_updated_clock_diff + CLOCK_SYNC_INTERVAL
< ts.tv_sec) {
struct timeval tv;
evutil_gettimeofday(&tv,NULL);
//tv_clock_diff記錄兩種時間的時間差
evutil_timersub(&tv, tp, &base->tv_clock_diff);
base->last_updated_clock_diff = ts.tv_sec;
}
return (0);
}
#endif
//如果所在的系統不支援monotonic時間,那麼只能使用evutil_gettimeofday了
return (evutil_gettimeofday(tp, NULL));
}
上面的程式碼雖然首先是使用cache時間,但實際上event_base結構體的cache時間也是通過呼叫gettime函式而得到的。上面程式碼也可以看到:如果所在的系統沒有提供monotonic時間,那麼就只能使用evutil_gettimeofday這個函式提供的系統時間了。
從上面的分析可知,如果Libevent所在的系統支援monotonic時間,那麼根本就不用考慮使用者手動修改系統時間這坑爹的事情。但如果所在的系統沒有支援monotonic時間,那麼Libevent就只能使用evutil_gettimeofday獲取一個使用者能修改的時間。
儘可能精確記錄時間差:
現在來看一下Libevent在這種情況下在怎麼解決這個坑爹得的問題。
Libevent給出的方案是,儘可能精確地計算 使用者往回調了多長時間。如果知道了使用者往回調了多長時間,那麼將小根堆中的全部event的時間都往回調一樣的時間即可。Libevent呼叫timeout_correct函式處理這個問題。
//event.c檔案
static void
timeout_correct(struct event_base *base, struct timeval *tv)
{
/* Caller must hold th_base_lock. */
struct event **pev;
unsigned int size;
struct timeval off;
int i;
//如果系統支援monotonic時間,那麼就不需要校準時間了
if (use_monotonic)
return;
//獲取現在的系統時間
gettime(base, tv);
//tv的時間更大,說明使用者沒有往回調系統時間。那麼不需要處理
if (evutil_timercmp(tv, &base->event_tv, >=)) {
base->event_tv = *tv;
return;
}
evutil_timersub(&base->event_tv, tv, &off);//off差值,即使用者調小了多少
pev = base->timeheap.p;
size = base->timeheap.n;
//使用者已經修改了OS的系統時間。現在需要對小根堆的所有event
//都修改時間。使得之適應新的系統時間
for (; size-- > 0; ++pev) {
struct timeval *ev_tv = &(**pev).ev_timeout;
//前面已經用off儲存了,使用者調小了多少。現在只需
//將小根堆的所有event的超時時間(絕對時間)都減去這個off即可
evutil_timersub(ev_tv, &off, ev_tv);
}
//儲存現在的系統時間。以防使用者再次修改系統時間
base->event_tv = *tv;
}
Libevent用event_base的成員變數event_tv儲存使用者修改系統時間前的系統時間。如果剛儲存完,使用者就修改系統時間,這樣就能精確地計算出使用者往回調了多長時間。但畢竟Libevent是使用者態的庫,不能做到使用者修改系統時間前的一刻儲存系統時間。
於是Libevent採用多采點的方式,即時不時就儲存一次系統時間。所以在event_base_loop函式中的while迴圈體裡面會有gettime(base, &base->event_tv);這是為了能多采點。但這個while迴圈裡面還會執行多路IO複用函式和處理被啟用event的回撥函式(這個回撥函式執行多久也是個未知數)。這兩個函式的執行需要的時間可能會比較長,如果使用者剛才是在執行完這兩個函式之後修改系統時間,那麼event_tv儲存的時間就不怎麼精確了。這也是沒有辦法的啊!!唉!!
下面貼出event_base_loop函式
//event.c檔案
int
event_base_loop(struct event_base *base, int flags)
{
const struct eventop *evsel = base->evsel;
struct timeval tv;
struct timeval *tv_p;
int res, done, retval = 0;
//要使用cache時間,得在配置event_base時,沒有加入EVENT_BASE_FLAG_NO_CACHE_TIME選項
clear_time_cache(base);
while (!done) {
timeout_correct(base, &tv);
tv_p = &tv;
if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
//參考http://blog.csdn.net/luotuo44/article/details/38637671
timeout_next(base, &tv_p); //獲取dispatch的最大等待時間
} else {
evutil_timerclear(&tv);
}
//儲存系統時間。如果有cache,將儲存cache時間。
gettime(base, &base->event_tv);
//之所以要在進入dispatch之前清零,是因為進入
//dispatch後,可能會等待一段時間。cache就沒有意義了。
//如果第二個執行緒此時想add一個event到這個event_base裡面,在
//event_add_internal函式中會呼叫gettime。如果cache不清零,
//那麼將會取這個cache時間。這將取一個不準確的時間。
clear_time_cache(base);
//多路IO複用函式
res = evsel->dispatch(base, tv_p);
//將系統時間賦值到cache中
update_time_cache(base);
//處理超時事件。參考http://blog.csdn.net/luotuo44/article/details/38637671
timeout_process(base);
if (N_ACTIVE_CALLBACKS(base)) {
int n = event_process_active(base);//處理啟用event
}
}
return (retval);
}
可以看到,在dispatch和event_process_active之間有一個update_time_cache。而前面的gettime(base,&base->event_tv);實際上取的就是cache的時間。所以,如果該Libevent支援cache的話,會精確那麼一些。一般來說,使用者為event設定的回撥函式,不應該執行太久的時間。這也是tv_cache時間的另外一個作用。
出現的bug:
由於Libevent的解決方法並不是很精確,所以還是會有一些bug。下面給出一個bug。如果使用者是在呼叫event_new函式之後,event_add之前對系統時間進行修改,那麼無論使用者設定的event超時有多長,都會馬上觸發超時。下面給出實際的例子。這個例子要執行在不支援monotonic時間的系統,我是在Windows執行的。#include <event2/event.h>
#include<stdio.h>
void timeout_cb(int fd, short event, void *arg)
{
printf("in the timeout_cb\n");
}
int main()
{
struct event_base *base = event_base_new();
struct event *ev = event_new(base, -1, EV_TIMEOUT, timeout_cb, NULL);
int ch;
//暫停,讓使用者有時間修改系統時間。可以將系統時間往前1個小時
scanf("%c", &ch);
struct timeval tv = {100, 0};//這個超時時長要比較長。這裡取100秒
//第二個引數不能為NULL.不然也是不能觸發超時的。畢竟沒有時間
event_add(ev, &tv);
event_base_dispatch(base);
return 0;
}
這個bug的出現是因為,在event_base_new_with_config函式中有gettime(base,&base->event_tv),所以event_tv記錄了修改前的時間。而event_add是在修改系統時間後才呼叫的。所以event結構體的ev_timeout變數使用的是修改系統時間後的超時時間,這是正確的時間。在執行timeout_correct函式時,Libevent發現使用者修改了系統時間,所以就將本來正確的ev_timeout減去了off。所以ev_timeout就變得比較修改後的系統時間小了。在後面檢查超時時,就會發現該event已經超時了(實際是沒有超時),就把它觸發。
如果該event有EV_PERSIST屬性,那麼之後的超時則會是正確的。這個留給讀者去分析吧。
另外,Libevent並沒有考慮把時鐘往後調,比如現在是9點,使用者把系統時間調成10點。上面的程式碼如果使用者是在event_add之後修改系統時間,就能發現這個bug。