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 voidfd_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 voidfd_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監控的條件。整個注冊示意圖如下:
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 voidfd_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 voidfd_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 noinlineev_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也是一樣,等分析到那個類型的監控器類型時在作展開。這里用個圖梳理下結構。
最后在循環中執行宏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”的優先級模型。
寫這篇博客主要是為了做一個學習記錄,里邊肯定會有很多錯誤。學習IO事件時,查閱了不少博文,這幾篇的幫組很大,多向大牛學習,文中也大量引用了他們博文中的圖片和例子,如有不妥,請告之
http://my.oschina.net/u/917596/blog/177030
https://cnodejs.org/topic/4f16442ccae1f4aa270010a3
新聞熱點
疑難解答