天天看點

[記錄]libev代碼分析[初稿,内容不完善]

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如何管理使用者感興趣描述符,以及描述符對應的感興趣事件。

[記錄]libev代碼分析[初稿,内容不完善]

圖 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