1. 程式人生 > >Libevent原始碼分析-----Libevent時間管理

Libevent原始碼分析-----Libevent時間管理

基本時間操作函式:

        Libevent採用的時間型別是struct  timeval,這個型別在很多平臺都提供了。此外,Libevent還提供了一系列的時間操作函式。比如兩個struct timeval相加、相減、比較大小。有些平臺直接提供了一些時間操作函式,但有些則沒有,那麼Libevent就自己實現。這些巨集如下:

#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-config.h指明所在系統的環境》一文。

        Libevent的時間一般是用在超時event的。對於超時event,使用者只需給出一個超時時間,比如多少秒,而不是一個絕對時間。但在Libevent內部,要將這個時間轉換成絕對時間。所以在Libevent內部會經常獲取系統時間(絕對時間),然後進行一些處理,比如,轉換、比較。

cache時間:

        Libevent封裝了一個evutil_gettimeofday函式用來獲取系統時間,該函式在POSIX的系統是直接呼叫gettimeofday函式,在Windows系統是通過_ftime函式。雖然gettimeofday的
耗時成本不大
,不過Libevent還是使用了一個cache儲存時間,使得更加高效。在event_base結構體有一個struct timeval型別的cache變數 tv_cache。處理超時event的兩個函式event_add_internal和event_base_loop內部都是呼叫gettime函式獲取時間的。gettime函式如下:
//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。