1. 程式人生 > >libev原始碼分析--事件監控器

libev原始碼分析--事件監控器

另外兩個重要的監控器

前面通過IO監控器將Libev的整個工作流程過了一遍。中間濾過了很多與其他事件監控器相關的部分,但是整體思路以及很明晰了,只要針對其他型別的watcher看下其初始化和註冊過程以及在ev_run中的安排即可。這裡我們再分析另兩個常用的watcher

1.分析定時器監控器

定時器在程式中可以做固定週期tick操作,也可以做一次性的定時操作。Libev中與定時器類似的還有個週期事件watcher。其本質都是一樣的,只是在時間的計算方法上略有不同,並有他自己的一個事件管理的堆。對於定時器事件,我們按照之前說的順序從ev_init開始看起。

1.1定時器監控器的初始化

定時器初始化使用 ev_init(&timer_w,timer_action);

,這個過程和之前的IO類似,主要就是設定基類的active、pending、priority以及觸發動作回撥函式cb。

1.2設定定時器監控器的觸發條件

通過 ev_timer_set(&timer_w,2,0);可以設定定時器在2秒鐘後被觸發。如果第三個引數不是0而是一個大於0的正整數n時,那麼在第一次觸發(2秒後),每隔n秒會再次觸發定時器事件。

其為一個巨集定義 do { ((ev_watcher_time *)(ev))->at = (after_); (ev)->repeat = (repeat_); } while (0) 也就是設定派生類定時器watcher的“at”為觸發事件,以及重複條件“repeat”。

1.3將定時器註冊到事件驅動器上

ev_timer_start(main_loop,&timer_w);會將定時器監控器註冊到事件驅動器上。其首先 ev_at (w) += mn_now; 得到未來的時間,這樣放到時間管理的堆“timers”中作為權重。然後通過之前說過的“ev_start”修改驅動器loop的狀態。這裡我們又看到了動態大小的陣列了。Libev的堆的記憶體管理也是通過這樣的關係的。具體這裡堆的實現,感興趣的可以仔細看下實現。這裡的操作就是將這個時間權重放到堆中合適的位置。這裡堆單元的結構為:

1 typedef struct {
2 ev_tstamp at;
3 WT w;
4 } ANHE;

其實質就是一個時刻at上掛一個放定時器watcher的list。當超時時會依次執行這些定時器watcher上的觸發回撥函式。

1.4定時器監控器的觸發

最後看下在一個事件驅動器迴圈中是如何處理定時器監控器的。這裡我們依然拋開其他的部分,只找定時器相關的看。在“/ calculate blocking time/”塊裡面,我們看到計算blocking time的時候會先:

1 if (timercnt) {
2 ev_tstamp to = ANHE_at (timers [HEAP0]) - mn_now;
3 if(waittime > to) waittime = to;
4 }

如果有定時器,那麼就從定時器堆(一個最小堆)timers中取得堆頂上最小的一個時間。這樣就保證了在這個時間前可以從backend_poll中出來。出來後執行timers_reify處理將pengding的定時器。

timers_reify中依次取最小堆的堆頂,如果其上的ANHE.at小於當前時間,表示該定時器watcher超時了,那麼將其壓入一個數組中,由於在實際執行pendings二維陣列上對應優先順序上的watcher是從尾往頭方向的,因此這裡先用一個數組依時間先後次存下到一箇中間陣列loop->rfeeds中。然後將其逆序呼叫ev_invoke_pending插入到pendings二維陣列中。這樣在執行pending事件的觸發動作的時候就可以保證,時間靠前的定時器優先執行。函式feed_reversefeed_reverse_done就是將超時的定時器加入到loop->rfeeds暫存陣列以及將暫存陣列中的pending的watcher插入到pengdings陣列的操作。把pending的watcher加入到pendings陣列,後續的操作就和之前的一樣了。回依次執行相應的回撥函式。

這個過程中還判斷定時器的 w->repeat 的值,如果不為0,那麼會重置該定時器的時間,並將其壓入堆中正確的位置,這樣在指定的時間過後又會被執行。如果其為0,那麼呼叫ev_timer_stop關閉該定時器。 其首先通過clear_pending置pendings陣列中記錄的該watcher上的回撥函式為一個不執行任何動作的啞動作。

總結一下定時器就是在backend_poll之前通過定時器堆頂的超時時間,保證blocking的時間不超過最近的定時器時間,在backend_poll返回後,從定時器堆中取得超時的watcher放入到pendings二維陣列中,從而在後續處理中可以執行其上註冊的觸發動作。然後從定時器管理堆上刪除該定時器。最後呼叫和ev_start呼應的ev_stop修改驅動器loop的狀態,即loop->activecnt減少一。並將該watcher的active置零。

對於週期性的事件監控器是同樣的處理過程。只是將timers_reify換成了periodics_reify。其內部會對週期性事件監控器派生類的做類似定時器裡面是否repeat的判斷操作。判斷是否重新調整時間,或者是否重複等邏輯,這些看下程式碼比較容易理解,這裡不再贅述。·

2.分析訊號監控器

分析完了定時器的部分,再看下另一個比較常用的訊號事件的處理。Libev裡面的訊號事件和Tornado.IOLoop是一樣的,通過一個pipe的IO事件來處理。直白的說就是註冊一個雙向的pipe檔案物件,然後監控上面的讀事件,待相應的訊號到來時,就往這個pipe中寫入一個值然他的讀端的讀事件觸發,這樣就可以執行相應註冊的觸發動作回撥函數了。

我們還是從初始化-》設定觸發條件-》註冊到驅動器-》觸發過程這樣的順序介紹。

2.1訊號監控器的初始化

ev_init(&signal_w,signal_action);這個函式和上面的一樣不用說了

2.2設定訊號監控器的觸發條件

ev_signal_set(&signal_w,SIGINT);該函式設定了Libev收到SIGINT訊號是觸發註冊的觸發動作回撥函式。其操作和上面的一樣,就是設定了訊號監控器私有的(ev)->signum為標記。

2.3將訊號監控器註冊到驅動器上

這裡首先介紹一個數據結構:

1 typedef struct
2 {
3 EV_ATOMIC_T pending;
4 EV_P;
5 WL head;
6 } ANSIG;
7 static ANSIG signals [EV_NSIG -1];

EV_ATOMIC_T pending;可以認為是一個原子物件,對他的讀寫是原子的。一個表示事件驅動器的loop,以及一個watcher的連結串列。

ev_signal_start中,通過signals陣列儲存訊號監控單元。該陣列和anfds陣列類似,只是他以訊號值為索引。這樣可以立馬找到訊號所在的位置。從 Linux 2.6.27以後,Kernel提供了signalfd來為訊號產生一個檔案描述符從而可以用檔案複用機制epoll、select等來管理訊號。Libev就是用這樣的方式來管理訊號的。 這裡的程式碼用巨集控制了。其邏輯大體是這樣的

1 #if EV_USE_SIGNALFD
2 res = invoke_signalfd
3 # if EV_USE_SIGNALFD
4 if  (res is not valied)
5 # endif
6 {
7 use evpipe to instead
8 }

這個是框架。其具體的實現可以參考使用signalfd和evpipe_init實現。其實質就是通過一個類似於管道的檔案描述符fd,設定對該fd的讀事件監聽,當收到訊號時通過signal註冊的回撥函式往該fd裡面寫入,使其讀事件觸發,這樣通過backend_poll返回後就可以處理ev_init為該訊號上註冊的觸發回撥函數了。

在函式evpipe_init裡面也用了一個可以學習的技巧,和上面的#if XXX if() #endif {} 一樣,處理了不支援eventfd的情況。eventfd是Kernel 2.6.22以後才支援的系統呼叫,用來建立一個事件物件實現,程序(執行緒)間的等待/通知機制。他維護了一個可以讀寫的檔案描述符,但是隻能寫入8byte的內容。但是對於我們的使用以及夠了,因為這裡主要是獲得其可讀的狀態。對於不支援eventfd的情況,則使用上面說過的,用系統的pipe呼叫產生的兩個檔案描述符分別做讀寫物件,來完成。

2.4訊號事件監控器的觸發

在上面設定訊號的pipe的IO事件是,根據使用的機制不同,其實現和觸發有點不同。對於signalfd。

1 ev_io_init (&sigfd_w, sigfdcb, sigfd, EV_READ); /*  for signalfd */
2 ev_set_priority (&sigfd_w, EV_MAXPRI);
3 ev_io_start (EV_A_ &sigfd_w);

也就是註冊了sigfdcb函式。該函式:

1 ssize_t res = read (sigfd, si, sizeof (si));
2 for (sip = si; (char*)sip < (char *)si + res; ++sip)
3 ev_feed_signal_event (EV_A_ sip->ssi_signo);

首先將pipe內容讀光,讓後續的可以pengding在該fd上。然後對該signalfd上的所有訊號弟阿勇ev_feed_signal_event吧每個訊號上的ANSIG->head上掛的watcher都用ev_feed_event加入到pendings二維陣列中。這個過程和IO的完全一樣。

而對於eventfd和pipe則是:

1 ev_init (&pipe_w, pipecb);
2 ev_set_priority (&pipe_w, EV_MAXPRI);     
3 ev_io_set (&pipe_w, evpipe [0] <0 ? evpipe [1] : evpipe [0], EV_READ);
4 ev_io_start (EV_A_ &pipe_w);

pipe_w是驅動器自身的loop->pipe_w。併為其設定了回撥函式pipecb:

01 #if EV_USE_EVENTFD
02 if(evpipe [0] <0)
03 {
04 uint64_t counter;
05 read (evpipe [1], &counter, sizeof (uint64_t));
06 }
07 else
08 #endif
09 {
10 chardummy[4];     
11 read (evpipe [0], &dummy, sizeof (dummy));
12
13 }
14 ...
15 xxx
16 ...
17 for (i = EV_NSIG -1; i--; )
18 if(expect_false (signals [i].pending))
19 ev_feed_signal_event (EV_A_ i +1);

這裡將上面的技巧#if XXX if() #endif {}拓展為了#if XXX if() {} else #endif {} 。這裡和上面的操作其實是一樣的。後續操作和signalfd裡面一樣,就是讀光pipe裡面的內容,然後依次將watcher加入到pendings陣列中。