1. 程式人生 > >libev 中IO事件迴圈解析

libev 中IO事件迴圈解析

1、IO事件基本資料結構ev_io

struct ev_io這個結構體是IO監視器。libev中所有的事件均有自己的一個結構體來表示,如時間事件是ev_time、ev_io等。

基類ev_watcher定義如下:

typedef struct ev_watcher
{
	int active; 
	int pending;
	int priority;
	void *data; 
	void (*cb)(struct ev_loop *loop, struct ev_watcher *w, int revents);
}

基類中 “active"表示是否啟用該watcher,“pending”該監控器是否處於pending狀態,“priority"其優先順序以及觸發後執行的動作的回撥函式。

與基類配套的還有個裝監控器的List:

typedef struct ev_watcher_list
{
	int active; 
	int pending;
	int priority;
	void *data; 
	void (*cb)(struct ev_loop *loop, struct ev_watcher_list *w, int revents);
	struct ev_watcher_list *next;
} ev_watcher_list;

ev_io是對一個IO事件監視的基礎結構體。定義如下:

typedef struct ev_io
{
	int active; 
	int
pending; int priority; void *data; void (*cb)(struct ev_loop *loop, struct ev_io *w, int revents); struct ev_watcher_list *next; int fd; /* 這裡的fd,events就是派生類的私有成員,分別表示監聽的檔案fd和觸發的事件(可讀還是可寫) */ int events; } ev_io;

原始碼裡ev_io定義在ev.h中。原文定義中嵌套了一些基類和其他一些巨集定義,這裡直接寫出來,方便理解。可以看到將派生類的私有變數放在了共有部分的後面。這樣,當使用C的指標強制轉換後,一個指向 struct ev_io物件的基類 ev_watcher 的指標p就可以通過 p->active 訪問到派生類中同樣表示active的成員了。

2、IO事件的初始化和設定

初始化和設定比較簡單,如下:

#define ev_io_init(ev,cb,fd,events)          do { ev_init ((ev), (cb)); ev_io_set ((ev),(fd),(events)); } while (0)
#define ev_io_set(ev,fd_,events_)            do { (ev)->fd = (fd_); (ev)->events = (events_) | EV__IOFDSET; } while (0)

初始化一個IO事件,只需要呼叫ev_io_init()函式,引數ev表示ev_io指標,cb表示觸發事件的回撥函式,fd表示要監視的檔案描述符,events表示監視的事件。

3、IO事件的註冊

先了解 struct ANFD,ANFD表示事件迴圈中對一個檔案描述符fd的監視的基本資訊結構體,定義如下:

typedef struct
{
  WL head;//watch_list結構體
  unsigned char events; /* 所監視的事件 */
  unsigned char reify;  /* 標誌位,用來標記ANFD需要被重新例項化(EV_ANFD_REIFY, EV__IOFDSET) */
  unsigned char emask;  /* the epoll backend stores the actual kernel mask in here */
  unsigned char unused;
  unsigned int egen;    /* generation counter to counter epoll bugs */
} ANFD;  /* 這裡去掉了對epoll的判斷和windows的IOCP*/

首先是WL head 這個基類監視器連結串列,這裡首先只用關注一個 “head” ,他是之前說過的wather的基類連結串列。 這裡一個ANFD就表示對一個檔案描述符的監控 ,那麼對該檔案描述的可讀還是可寫監控,監控的動作是如何定義的,就是通過這個連結串列,(這個連結串列的長度一般不會超過3,檔案的監控條件無非是可讀、可寫等)把對該檔案描述法的監控器都掛上去,這樣就可以通過檔案描述符找到了。而前面的說的anfds就是這個物件的陣列,下標通過檔案描述符fd進行索引。anfds是一個ANFD型動態陣列。這樣anfds陣列就是全部的IO監控,最後可以通過epoll_wait()來監測事件。

每當有新的IO監視器fd加入,呼叫wlist_add()新增到anfds[fd]的連結串列head中。如果一個anfds的元素監控條件發生改變,如何修改這個元素的監控條件呢。anfds的下標可以用fd來表示,這裡有一個新的陣列,陣列元素內容是新新增的要監視的IO事件的fd或者修改監視內容的fd,陣列名是fdchanges,也是動態陣列。這個陣列記錄了新加入fd或者修改的fd的值,具體實現函式為“fd_change”

inline_size void
fd_change (EV_P_ int fd, int flags)
{
  unsigned char reify = anfds [fd].reify;
  anfds [fd].reify |= flags;//標誌,表示fd監視條件被修改了

  if (expect_true (!reify))//如果fd最初的監視條件為空,表示新加入的fd
    {
      ++fdchangecnt;//fd計數器加一
      array_needsize (int, fdchanges, fdchangemax, fdchangecnt, EMPTY2);//新增到fdchanges陣列中
      fdchanges [fdchangecnt - 1] = fd;
    }
  //如果不是新加入的fd,則fdchanges陣列中已經有fd了。表示以前新增過對fd的IO監視
}

這時所有的要被監視的fd都存放在fdchanges陣列中,當我們執行ev_run時,會呼叫“fd_reify”,它遍歷fdchanges陣列,如果發現fd的監視條件發生變化了,就會呼叫epoll_ctl()函式來改變fd的監視狀態。 這個fdchanges陣列的作用就在於此,他記錄了anfds陣列中的watcher監控條件可能被修改的檔案描述符,並在適當的時候將呼叫系統的epoll_ctl或則其他檔案複用機制修改系統監控的條件。 注意,假如我們在某個fd 上已經有個 watch 註冊 了 read 事件,這時我們又再新增一個watch,還是read 事件,但是不同的回撥函式,在此種情況下,我們不應該呼叫epoll_ctrl 之類的系統呼叫(減少系統開銷),因為我們的events 集合是沒有改變的(表示監視的事件沒有發生改變),所以為了達到這個目,anfd[fd] 結構體中還有一個events事件,它是原先的所有watcher 的事件的 ”|“ 操作,向系統的epoll 重新新增描述符的操作 是在下次事件迭代開始前進行的,當我們依次掃描fdchangs,找到對應的anfd 結構,如果發現先前的events 與 當前所有的watcher 的”|“ 操作結果不等,則表示我們需要呼叫epoll_ctrl 之類的函式來進行更改,反之不做操作即,作為一條原則,在呼叫系統呼叫前,我們已經做了充分的檢查,確保不進行多餘的系統呼叫! fd_reify()中定義如下:

inline_size void
fd_reify (EV_P)
{
  int i;
  for (i = 0; i < fdchangecnt; ++i)
    {
      int fd = fdchanges [i];//取出可能改變監控條件的fd
      ANFD *anfd = anfds + fd;//得到anfds中下標
      ev_io *w;//頂一個ev_io指標

      unsigned char o_events = anfd->events;
      unsigned char o_reify  = anfd->reify;

      anfd->reify  = 0;

      /*if (expect_true (o_reify & EV_ANFD_REIFY)) probably a deoptimisation */
        {
          anfd->events = 0;

          for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)//這裡用到了強制轉換,for迴圈的作用就是
          //獲得fd全部的新的監控事件集合,存放在events成員變數中
            anfd->events |= (unsigned char)w->events;

          if (o_events != anfd->events)//如果新監控事件和舊監控事件不同,
            o_reify = EV__IOFDSET; /* actually |= *///修改標誌位,表示fd監控條件改變
        }

      if (o_reify & EV__IOFDSET)//fd監控條件改變,呼叫backend_modify也就是epoll_ctl()修改fd的監控條件
        backend_modify (EV_A_ fd, o_events, anfd->events);
    }

  fdchangecnt = 0;//一次遍歷完成,fdchanges陣列個數清零
}

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

111

4、啟動IO事件驅動器

啟動IO事件驅動器,ev_run中主要呼叫了fd_reify()後,做了一些時間計算後,進入了backend_poll也就是epoll_poll()中,執行了wait操作

eventcnt = epoll_wait (backend_fd, epoll_events, epoll_eventmax, timeout * 1e3);

成功的話,返回了響應事件的個數,然後執行了fd_event()

inline_speed void
fd_event (EV_P_ int fd, int revents)
{
/* do not submit kernel events for fds that have reify set */
/* because that means they changed while we were polling for new events */
ANFD *anfd = anfds + fd;
 if (expect_true (!anfd->reify))//reify是0
/*如果reify不是0,則表示我們添加了新的事件在fd上,不是很懂*/
    fd_event_nocheck (EV_A_ fd, revents);
}
fd_event_nocheck 如下
inline_speed void
fd_event_nocheck (EV_P_ int fd, int revents)
{
  ANFD *anfd = anfds + fd;
  ev_io *w;

  for (w = (ev_io *)anfd->head; w; w = (ev_io *)((WL)w)->next)//對fd上的監視器依次做檢測,
    {
      int ev = w->events & revents;//相應的事件被觸發了

      if (ev)//pending條件滿足,監控器加入到pendings陣列中pendings[pri]上的pendings[pri][old_lenght+1]的位置上
ev_feed_event (EV_A_ (W)w, ev);
    }
}
void noinline
ev_feed_event (EV_P_ void *w, int revents) EV_THROW
{
  W w_ = (W)w;
  int pri = ABSPRI (w_);

  if (expect_false (w_->pending))
    pendings [pri][w_->pending - 1].events |= revents;
  else
    {
      w_->pending = ++pendingcnt [pri];
      array_needsize (ANPENDING, pendings [pri], pendingmax [pri], w_->pending, EMPTY2);
      pendings [pri][w_->pending - 1].w      = w_;
      pendings [pri][w_->pending - 1].events = revents;
    }

  pendingpri = NUMPRI - 1;
}

以epoll 為例,當epoll_wait 返回一個fd_event 時 ,我們就可以直接定位到對應fd 的 watch list ,這個watch list 的長度一般不會超過3 ,fd_event 會有一個導致觸發的事件 ,我們用這個事件依次和各個watch 註冊的 event 做 “&” 操作, 如果不為0 ,則把對應的watch 加入到 待處理佇列pendings中(當我們啟用watcher 優先順序模式時,pendings 是個2維陣列,此時僅考慮普通模式)

這裡要介紹一個新的資料結構,他表示pending中的wather也就是監控條件滿足了,但是還沒有觸發動作的狀態。

typedef struct
{
  W w;
  int events; /* the pending event set for the given watcher */
} ANPENDING;

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

215034_LAfF_917596

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

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

5、總結下

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

215211_W3Cy_917596

寫這篇部落格主要是為了做一個學習記錄,裡邊肯定會有很多錯誤。學習IO事件時,查閱了不少博文,這幾篇的幫組很大,多向大牛學習,文中也大量引用了他們博文中的圖片和例子,如有不妥,請告之