1. 程式人生 > >libev原始碼分析--設計思想

libev原始碼分析--設計思想

Libev設計思路

理清了Libev的程式碼結構和主要的資料結構,就可以跟著示例中介面進入到Libev中,跟著程式碼瞭解其設計的思路。這裡我們管struct ev_loop稱作為事件迴圈驅動器而將各種watcher稱為事件監控器。

1.分析例子中的IO事件

這裡在前面的例子中我們先把定時器和訊號事件的使用註釋掉,只看IO事件監控器,從而瞭解Libev最基本的邏輯。可以結合Gdb設斷點一步一步的跟看看程式碼的邏輯是怎樣的。

我們從main開始一步步走。首先執行 struct ev_loop *main_loop = ev_default_loop(0); 通過跟進程式碼可以跟到函式ev_default_loop

裡面去,其主要邏輯,就是全域性物件指標ev_default_loop_ptr若為空,也就是不曾使用預製的驅動器時,就讓他指向全域性物件default_loop_struct,同時在本函式裡面統一用名字"loop"來表示該預製驅動器的指標。從而與函式引數為EV_P 以及 EV_A的寫法配合。接著對該指標做 loop_init操作,即初始化預製的事件驅動器。這裡函式的呼叫了就是用到了EV_A_ 這樣的寫法進行簡化。初始化之後如果配置中Libev支援子程序,那麼通過訊號監控器實現了子程序監控器。這裡可以先不用去管他,知道這段程式碼作用即可。 這裡再Libev的函式定義的時候,會看到 “EV_THROW” 這個東西,這裡可以不用管它,他是對CPP中"try … throw"的支援,和EV_CPP(extern "C" {)
這樣不同尋常的 extern “C” 一樣是一種編碼技巧。現在我們以分析設計思路為主。在瞭解了總體後,可以再對其編碼技巧進行梳理。否則的話看一份程式碼會非常吃力,而且速度慢。甚至有的時候這些“hacker”並不一定是有益的。

1.1驅動器的初始化

下面看下驅動器的初始化過程中都做了哪些事情。首先最開始的一段程式碼判斷系統的clock_gettime是否支援CLOCK_REALTIME和CLOCK_MONOTONIC。這兩種時間的區別在於後者不會因為系統時間被修改而被修改,詳細解釋可以參考man page 。接著判斷環境變數對驅動器的影響,這個在官方的Manual中有提到,主要就是影響預設支援的IO複用機制。接著是一連串的初始值的賦值,開始不用瞭解其作用。在後面的分析過程中便可以知道。接著是根據系統支援的IO複用機制,對其進行初始化操作。這裡可以去"ev_epoll.c” 和"ev_select.c"中看一下。 最後是判斷如果系統需要訊號事件,那麼通過一個PIPE的IO事件來實現,這裡暫且不用管他,在理解了IO事件的實現後,自然就知道這裡他做了什麼操作。

對於"ev_epoll.c” 和"ev_select.c"中的 xxx_init 其本質是一致的,就像外掛一樣,遵循一個格式,然後可以靈活的擴充套件。對於epoll主要就是做了一個 epoll_create*的操作(epoll_create1可以支援EPOLL_CLOEXEC)。

1 backend_mintime = 1e-3;/* epoll does sometimes return early, this is just to avoid the worst */
2 backend_modify  = epoll_modify;
3 backend_poll    = epoll_poll;

這裡就可以看成是外掛的模板了,在後面會修改的時候呼叫backend_modify在poll的時候呼叫backend_poll.從而統一了操作。

1 epoll_eventmax = 64;/* initial number of events receivable per poll */
2 epoll_events = (struct epoll_event *)ev_malloc (sizeof (struct epoll_event) * epoll_eventmax)

這個就看做為是每個機制特有的部分。熟悉epoll的話,這個就不用說了。

對於select (Linux平臺上的)

1 backend_mintime = 1e-6;
2 backend_modify  = select_modify;
3 backend_poll    = select_poll;

這個和上面一樣,是相當於外掛介面

1 vec_ri  = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_ri);
2 vec_ro  = ev_malloc (sizeof (fd_set));
3 vec_wi  = ev_malloc (sizeof (fd_set)); FD_ZERO ((fd_set *)vec_wi);
4 vec_wo  = ev_malloc (sizeof (fd_set));

同樣,這個是select特有的,表示讀和寫的fd_set的vector,ri用來裝select返回後符合條件的部分。其他的如poll、kqueue、Solaris port都是類似的,可以自行閱讀。

1.2IO監控器的初始化

上面的過程執行完了ev_default_loop過程,然後到後面的ev_init(&io_w,io_action);,他不是一個函式,而是一個巨集定義:

1 ((ev_watcher *)(void*)(ev))->active = ((ev_watcher *)(void*)(ev))->pending = 0;
2 ev_set_priority ((ev), 0);
3 ev_set_cb ((ev), cb_);

這裡雖然還有兩個函式的呼叫,但是很好理解,就是設定了之前介紹的基類中 “active"表示是否啟用該watcher,“pending”該監控器是否處於pending狀態,“priority"其優先順序以及觸發後執行的動作的回撥函式。

1.3 設定IO事件監控器的觸發條件

在初始化監控器後,還要設定其監控監控的條件。當該條件滿足時便觸發該監控器上註冊的觸發動作。ev_io_set(&io_w,STDIN_FILENO,EV_READ);從引數邊可以猜出他幹了什麼事情。就是設定該監控器監控標準輸入上的讀事件。該呼叫也是一個巨集定義:

1 (ev)->fd = (fd_); (ev)->events = (events_) | EV__IOFDSET;

就是設定派生類IO監控器特有的變數fd和events,表示監控那個檔案fd已經其上的可讀還是可寫事件。
%TODO:補上EV_IOFDSET的作用

1.4註冊IO監控器到事件驅動器上

準備好了監控器後就要將其註冊到事件驅動器上,這樣就形成了一個完整的事件驅動模型。 ev_io_start(main_loop,&io_w); 。這個函式裡面會第一次見到一個一個巨集 “EV_FREQUENT_CHECK”,是對函式 “ev_verify"的呼叫,那麼ev_verify是幹什麼的呢?用文件的話“This can be used to catch bugs inside libev itself”,如果看其程式碼的話,就是去檢測Libev的內部資料結構,判斷各邊界值是否合理,不合理的時候assert掉。在生產環境下,我覺得根據性格來對待。如果覺得他消耗資源(要檢測很多東西跑很多迴圈)可以編譯的時候關掉該定義。如果需要assert,可以在編譯的時候加上選項。

然後看到 ev_start 呼叫,該函式實際上就是給驅動器的loop->activecnt增一併置loop->active為真(這裡統一用loop表示全域性物件的預製驅動器物件default_loop_struct),他們分別表示事件驅動器上正監控的監控器數目以及是否在為監控器服務。

1 array_needsize (ANFD, anfds, anfdmax, fd +1, array_init_zero);
2 wlist_add (&anfds[fd].head, (WL)w);

感興趣的可以去看下Libev裡麼動態調整陣列的實現。這裡我們主要看整體邏輯。他的工作過程是先判斷陣列anfds是否還有空間再加對檔案描述符fd的監控,,沒有的話則調整陣列的記憶體大小,使其大小足以容下。

這裡要介紹下之前沒有介紹的一個數據結構,這個沒有上下文比較難理解,因此放在這裡介紹。

1 typedef struct
2 {
3 WL head;
4 unsigned char events; /* the events watched for */
5 unsigned char reify;  /* flag set when this ANFD needs reification (EV_ANFD_REIFY, EV__IOFDSET) */
6 unsigned char emask;  /* the epoll backend stores the actual kernel mask in here */
7 unsigned char unused;
8 unsigned int egen;    /* generation counter to counter epoll bugs */
9 } ANFD;  /* 這裡去掉了對epoll的判斷和windows的IOCP*/

這裡首先只用關注一個 “head” ,他是之前說過的wather的基類連結串列。這裡一個ANFD就表示對一個檔案描述符的監控,那麼對該檔案描述的可讀還是可寫監控,監控的動作是如何定義的,就是通過這個連結串列,把對該檔案描述法的監控器都掛上去,這樣就可以通過檔案描述符找到了。而前面的說的anfds就是這個物件的陣列,下標通過檔案描述符fd進行索引。在Redis-ae那篇文章中已經討論過這樣的可以達到O(1)的索引速度而且空間佔用也是合理的。

接著的“fd_change”與“fd_reify”是呼應的。前者將fd新增到一個fdchanges的陣列中,後者則依次遍歷這個陣列中的fd上的watcher與anfds裡面對飲的watcher進行對比,判斷監控條件是否改變了,如果改變了則呼叫backend_modify也就是epoll_ctl等調整系統對該fd的監控。這個fdchanges陣列的作用就在於此,他記錄了anfds陣列中的watcher監控條件可能被修改的檔案描述符,並在適當的時候將呼叫系統的epoll_ctl或則其他檔案複用機制修改系統監控的條件。這裡我們把這兩個主要的物理結構梳理下:
anfds的結構

總結一下注冊過程就是通過之前設定了監控條件IO watcher獲得監控的檔案描述符fd,找到其在anfds中對應的ANFD結構,將該watcher掛到該結構的head鏈上。由於對應該fd的監控條件有改動了,因此在fdchanges陣列中記錄下該fd,在後續的步驟中呼叫系統的介面修改對該fd監控的條件。

1.5 啟動事件驅動器

一切準備就緒了就可以開始啟動事情驅動器了。就是 ev_run。 其邏輯很清晰。就是

1 do{
2 xxxx;
3 backend_poll();  
4 xxxx
5 }while(condition_is_ok)

迴圈中開始一段和fork 、 prepare相關這先直接跳過,到分析與之相關的監控事件才去看他。直接到 /* calculate blocking time */ 這裡。熟悉事件模型的話,這裡還是比較常規的。就是從定時器堆中取得最近的時間(當然這裡分析的時候沒有定時器)與loop->timeout_blocktime比較得到阻塞時間。這裡如果設定了驅動器的io_blocktime,那麼在進入到poll之前會先sleep io_blocktime時間從而等待IO或者其他要監控的事件準備。這裡進入到backend_poll中的阻塞時間是包括了io_blocktime的時間。然後進入到backend_poll中。對於epoll就是進入到epoll_wait裡面。

epoll(或者select、kqueue等)返回後,將監控中的檔案描述符fd以及其pending(滿足監控)的條件通過 fd_event做一個監控條件是否改變的判斷後到fd_event_nocheck裡面對anfds[fd]陣列中的fd上的掛的監控器依次做檢測,如果pending條件符合,便通過ev_feed_event將該監控器加入到pendings陣列中pendings[pri]上的pendings[pri][old_lenght+1]的位置上。這裡要介紹一個新的資料結構,他表示pending中的wather也就是監控條件滿足了,但是還沒有觸發動作的狀態。

1 typedef struct
2 {
3 W w;
4 intevents; /* the pending event set for the given watcher */
5 } ANPENDING;

這裡 W w應該知道是之前說的基類指標。pendings就是這個型別的一個二維陣列陣列。其以watcher的優先順序為一級下標。再以該優先順序上pengding的監控器數目為二級下標,對應的監控器中的pending值就是該下標加一的結果。其定義為ANPENDING *pendings [NUMPRI]。同anfds一樣,二維陣列的第二維 ANPENDING *是一個動態調整大小的陣列。這樣操作之後。這個一系列的操作可以認為是fd_feed的後續操作,xxx_reify目的最後都是將pending的watcher加入到這個pengdings二維陣列中。後續的幾個xxx_reify也是一樣,等分析到那個型別的監控器型別時在作展開。
這裡用個圖梳理下結構。
pendings結構梳理圖

最後在迴圈中執行巨集EV_INVOKE_PENDING,其實是呼叫loop->invoke_cb,如果沒有自定義修改的話(一般不會修改)就是呼叫ev_invoke_pending。該函式會依次遍歷二維陣列pendings,執行pending的每一個watcher上的觸發動作回撥函式。

至此一次IO觸發過程就完成了。

2總結出Libev的設計思路

在Libev中watcher要算最關鍵的資料結構了,整個邏輯都是圍繞著watcher做操作。Libev內部維護一個基類ev_wathcer和若干個特定監控器的派生類ev_xxx。在使用的時候首先生成一個特定watcher的例項。並通過該派生物件私有的成員設定其觸發條件。然後用anfds或者最小堆管理這些watchers。然後Libev通過backend_poll以及時間堆管理運算出pending的watcher。然後將他們加入到一個以優先順序為一維下標的二維陣列。在合適的時間依次呼叫這些pengding的watcher上註冊的觸發動作回撥函式,這樣便可以按優先順序先後順序實現“only-for-ordering”的優先順序模型。
思路流程圖