libev原始碼分析--事件監控器
另外兩個重要的監控器
前面通過IO監控器將Libev的整個工作流程過了一遍。中間濾過了很多與其他事件監控器相關的部分,但是整體思路以及很明晰了,只要針對其他型別的watcher看下其初始化和註冊過程以及在ev_run中的安排即可。這裡我們再分析另兩個常用的watcher
1.分析定時器監控器
定時器在程式中可以做固定週期tick操作,也可以做一次性的定時操作。Libev中與定時器類似的還有個週期事件watcher。其本質都是一樣的,只是在時間的計算方法上略有不同,並有他自己的一個事件管理的堆。對於定時器事件,我們按照之前說的順序從ev_init開始看起。
1.1定時器監控器的初始化
定時器初始化使用 ev_init(&timer_w,timer_action);
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_reverse
和
feed_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 |
char dummy[ 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陣列中。