libev是高性能事件循環/事件模型的網絡庫,并且包含大量新特性[1]。其home-pages如是介紹:
Afull-featured and high-performance eventloop that is loosely modelled after libevent, but without its limitations andbugs.
網上有大量的代碼分析,我在學習libev的過程中也閱讀了這些部落格。對我分析libev代碼有很大幫助,這裡就不一一列舉這些優秀的博文了。本文旨在記錄學習的心得,錘煉表達能力與加深了解。
本文較為詳細分析libev的實作細節。首先,通過一個例子[1]開始分析旅程吧。
// a single header file is required
#include <ev.h>
#include <stdio.h> // for puts
// every watcher type has its own typedef'dstruct
// with the name ev_TYPE
ev_io stdin_watcher;
ev_timer timeout_watcher;
// all watcher callbacks have a similarsignature
// this callback is called when data isreadable on stdin
static void
stdin_cb (EV_P_ ev_io *w, int revents)
{
puts ("stdin ready");
// for one-shot events, one must manuallystop the watcher
// with its corresponding stop function.
ev_io_stop (EV_A_ w);
// this causes all nested ev_run's to stopiterating
ev_break (EV_A_ EVBREAK_ALL);
}
// another callback, this time for atime-out
static void
timeout_cb (EV_P_ ev_timer *w, int revents)
{
puts ("timeout");
// this causes the innermost ev_run tostop iterating
ev_break (EV_A_ EVBREAK_ONE);
}
int
main (void)
{
// use the default event loop unless youhave special needs
struct ev_loop *loop = ev_loop_new(0);
// initialise an io watcher, then start it
// this one will watch for stdin to becomereadable
ev_io_init (&stdin_watcher, stdin_cb,/*STDIN_FILENO*/ 0, EV_READ);
ev_io_start (loop, &stdin_watcher);
// initialise a timer watcher, then startit
// simple non-repeating 5.5 second timeout
ev_timer_init (&timeout_watcher, timeout_cb,5.5, 0.);
ev_timer_start (loop,&timeout_watcher);
// now wait for events to arrive
ev_run (loop, 0);
// break was called, so exit
return 0;
}
該例子來源于libev的man-pages。在例stdin上注冊一個可讀watcher, 當stdin上有資料可讀後,就會觸發watcher的可讀事件,相應的回調函數(例子中的回調函數是stdin_cb)就會被調用。
1)ev_loop_new
該函數傳回一個ev_loop的結構體。簡單分析下該結構體的成員變量,見注釋。
struct ev_loop {
// 與時間相關的變量
ev_tstampev_rt_now;
ev_tstampnow_floor;
ev_tstamp mn_now;
ev_tstamprtmn_diff;
//
ev_tstampio_blocktime;
ev_tstamptimeout_blocktime;
intbackend;//IO複用機制類型,epoll, select或者其他
int activecnt;//感興趣描述符總數
sig_atomic_t volatile loop_done;//事件循環結束标志
//描述符,linux中一切皆檔案
intbackend_fd;
ev_tstampbackend_fudge;
//當在描述符上注冊事件,或者修改已注冊事件,均會調用該函數
void(*backend_modify)(struct ev_loop *loop, int fd, int oev, int nev);
void(*backend_poll)(struct ev_loop *loop, ev_tstamp timeout);//阻塞目前線程,等待事件
//
ANFD*anfds;//記錄有關描述符的資訊,每一個描述符可以有多個watcher, 每個watcher可以有不同的事件類型
int anfdmax;//是anfds數組的長度
//
ANPENDING*pendings [NUMPRI];//有事件觸發的描述符集合,libev将事件具有優先級熟悉,使用者可以設定優先級
intpendingmax [NUMPRI];//每一個優先級下觸發描述符的最大數量
intpendingcnt [NUMPRI];//本次觸發事件的描述符總數,按優先級存放
//
ev_preparepending_w;
//
structpollfd *polls;
intpollmax;
intpollcnt;
int*pollidxs;
intpollidxmax;
//使用epoll作為底層IO複用機制時,所需的變量
structepoll_event *epoll_events;
intepoll_eventmax;
int*fdchanges;//有改動的描述符
int fdchangemax;
intfdchangecnt;
//
unsignedint loop_count;
unsignedint loop_depth;
//
void*userdata;
void(*release_cb)(struct ev_loop *loop);
void(*acquire_cb)(struct ev_loop *loop);
void(*invoke_cb) (struct ev_loop *loop);
};
ev_loop_new函數首先建立一個ev_loop的結構體,并将每個成員變量初始化為0. 然後調用loop_init函數對成員變量設定預設值,其中成員變量invoke_cb設定為ev_invoke_pending函數。後文會再次介紹該函數。那libev是在那裡建立epoll描述符的呢? 答案是,loop_init函數調用epoll_init函數來完成這個事情的。那我們看看epoll_init作了哪些事情吧,上代碼:
int epoll_init(ev_loop *loop, int flags)
{
loop->backend_fd = epoll_create (256); //調用耳熟能詳的epoll_create函數
if(loop->backend_fd < 0)
return0;
fcntl(loop->backend_fd, F_SETFD, FD_CLOEXEC);
loop->backend_fudge = 0.; /* kernel sources seem to indicate thisto be zero */
loop->backend_modify = epoll_modify;
//注冊函數指針,當描述符發生變化,該函數就會被調用
loop->backend_poll =epoll_poll;//阻塞,等待事件觸發
//中2個變量擁有epoll_wait函數的參數,想必都熟悉吧
loop->epoll_eventmax = 64; /* initial number of events receivable perpoll */
loop->epoll_events = (struct epoll_event *)ev_malloc (sizeof (structepoll_event) * loop->epoll_eventmax);
//傳回底層IO複用機制的類型,此處是EPOLL
returnEVBACKEND_EPOLL;
}
從代碼中可以看出,ev_loop_new函數,建立了ev_loop結構體,記錄了事件循環所需的各種變量,并且建立了epoll描述符。
2)ev_io_init
該函數接受2個參數,ev_loop結構體和watcher。在本例中,對stdin注冊的watcher是ev_iowatcher。首先,我們還是看看這個watcher的資料結構定義吧:
typedef struct ev_io
{
intactive; //若為1,表示該watcher是活動的,即沒有被顯示stop掉
intpending; //若為1,表示該watcher感興趣的事件觸發,但還沒被處理
intpriority; //優先級
void*data; //回調函數所需的資料
void(*cb)(struct ev_loop *loop, struct ev_io *w, int revents); //回調函數
struct ev_watcher_list *next;//同一個描述符上可以注冊多個watcher,這些watcher挂在一個連結清單中,
intfd; //描述符
intevents; //事件類型
} ev_io;
一個watcher可以注冊多種有效的事件,這些事件存儲在成員變量events中。對ev_io結構了解後,那ev_io_init函數的功能就很明顯了。ev_io_init函數接受4個參數分别是代表iowatcher的ev_io結構體,檔案描述符,回調函數,事件類型。ev_io_init的所做的事情就是根據參數初始化ev_io結構體,其他成員變量設定為預設值。該函數所作的事情較少。此時,其成員變量active 為0.
3)ev_io_start
該函數接受兩參數,分别是ev_loop* loop, ev_io*watcher。該函數将已經初始化完成的watcher注冊到事件循環中。當相應的事件觸發後,該Watcher的回調函數被執行。這裡我就不粘貼該函數内容了。這裡,我主要想描述loop如何管理使用者感興趣描述符,以及描述符對應的感興趣事件。

圖 ev_loop 管理多個檔案描述符,每個檔案描述符可以注冊多個Watcher
ev_io_start将watcher添加到fd對應的watcher連結清單中,然後将watcher的active字段置為1.
4) ev_loop
該函數循環調用epoll_wait函數,若有事件發生,對應的回調函數會被調用。為了簡化代碼分析,我将ev_loop中非ev_io類型的watcher相關的代碼先人為去掉。上代碼先:
void ev_run (EV_P_ int flags)
{
++loop_depth;
loop_done = EVBREAK_CANCEL;
EV_INVOKE_PENDING; /* in case we recurse, ensure ordering stays nice and clean */
do {
if (expect_false (loop_done))
break;
/* update fd-related kernel structures */
fd_reify (EV_A);
/* calculate blocking time */
{
ev_tstamp waittime = 0.;
ev_tstamp sleeptime = 0.;
/* remember old timestamp for io_blocktime calculation */
ev_tstamp prev_mn_now = mn_now;
/* update time to cancel out callback processing overhead */
time_update (EV_A_ 1e100);
++loop_count;
backend_poll (EV_A_ waittime);
/* update ev_rt_now, do magic */
time_update (EV_A_ waittime + sleeptime);
}
EV_INVOKE_PENDING;
} while (expect_true (
activecnt
&& !loop_done
&& !(flags & (EVRUN_ONCE | EVRUN_NOWAIT))
));
if (loop_done == EVBREAK_ONE)
loop_done = EVBREAK_CANCEL;
--loop_depth;
}
宏EV_INVOKE_PENDING會調用ev_loop結構體中的ev_invoke_pending函數,該函數作的事情就是,對于已經有事件觸發的描述符,執行描述符對應的回調函數。在ev_run函數中,進入循環處,就執行一次ev_invoke_pending函數,是希望任何已經觸發的事件盡快得到處理。
進入循環體後,fd_reify函數檢查是否有新的watcher添加到ev_loop中,某個watcher被删除掉了。這個函數代碼代碼很簡潔,就不上代碼了。對于采用EPOLL機制來說,在某檔案描述符上添加或者删除事件,是通過調用epoll_ctl函數來實作的。由此可見,使用者向某個描述符注冊了一個watcher後,該watcher所感興趣的事件在loop循環中完成的。接下來,計數本次循環的wait_time。然後,調用backend_poll函數,參數wait_time已經計算。對于底層是epoll機制來說,backend_poll函數指針指向的是epoll_wait函數。若在wait_time時間内,所監控的檔案描述符上有事件觸發,則将events資料存放到ev_loop結構體的epoll_events數組中,epoll_eventmax是該數組的長度。
大家或許大概了解如何使用epoll(若不了解,建議先看看如何使用epoll, 在分析libev會比較容易),epoll_wait傳回一個代表事件的數組,ev_loop也需要掃描該數組,對于每一個事件,根據其fd, 以及事件類型,将注冊在fd上的watcher結構的指針,存放在ev_loop結構的pendings數組中。如果一個fd上注冊了2個watcher, 那麼pendings數組中會占兩項,也就是說pendings是按照watcher來組織的,而不是按照fd來組織的。總之,backend_poll函數會将所有觸發了的watcher添加到ev_loop結構的pendings數組中。
接下來需要作什麼事情,或許大家已經猜到了吧。對,就是掃描pendings數組,調用每一個Watcher的回調函數。這個任務有ev_invoke_pending函數執行。
5)ev_io_stop
當使用者調用該函數時,表示停用該Watcher。該函數首先去清除watcher的狀态,即将pending置為0。通過代碼可以看到,若該watcher上已觸犯了一個事件,但對應的回調函數還未執行,此時peding為0後,該事件的回調函數永遠不會執行了。接着,從對應的fd的watcher連結清單中删除該watcher, 将該watcher的active變量置為0,再調用epoll_ctl删除相應的事件。至此,該watcher的生命就結束了。
參考文獻:
[1] http://software.schmorp.de/pkg/libev.html