libev源码分析--常用的watcher
在上一篇文章里,我們分析了libev整體設計思想和主循環的工作原理,也提到了watcher是銜接開發者代碼的主要入口。watcher與開發者最接近,也與具體事件處理邏輯最接近。所以,watcher的具體實現,與性能的關系也相當密切。下面,我們就來分析一下,libev中常用的幾種watcher的設計與實現。
ev_io
ev_io與底層io
ev_io的主要使命就是監聽并響應指定文件描述fd上的讀寫事件。對fd的監聽工作,主要委托給底層的io庫來完成。libev對目前比較流行的io庫都提供了支持,如:select, epoll以及windows的iocp等。在這里libev使用了Adaptor模式,通過統一的適配層隱藏了底層io庫的細節。在loop初始化的時候(loop_init),會根據配置將函數指針綁定到底層的io庫函數對應的適配代碼上。所以,開發者可以很方便的把代碼切換到不同的底層實現上。相關的函數有:backend_modify,向底層庫注冊fd事件,如:epoll的epoll_ctl;backend_poll,向底層庫輪詢fd上是否有感興趣的事件發生,如:epoll的epoll_wait。適配器實現的代碼可以在ev_LIB.c中看到,LIB是io庫的名字,如:ev_epoll.c,ev_win32.c等。
ev_io的結構
| 1 2 3 4 5 6 7 | typedef struct ev_io { EV_WATCHER_LIST (ev_io) int fd; /* ro */ int events; /* ro */ } ev_io; |
其中,EV_WATCHER_LIST是EV_WATCHER結構的鏈表節點結構。fd是監聽的文件描述符,events是感興趣的事件。 ev_io從它誕生的那一刻起,便于文件描述符緊密結合在一起了。ev_io的實例被存儲在loop->anfds的結構中。anfds的結構如下圖所示:
anfds其實是一個數組,它使用fd作為下標,數組中的元素是一個ANFD的結構。ANFD是一個維護fd信息的結構體。其中,events記錄了當前fd上感興趣的事件的記錄。head是watcher列表的頭指針,這個列表就是在這個fd上注冊的watcher列表。當fd的大小超出了anfds的容量,anfds會進行相應的擴展。
anfds可以理解成一個簡易的map,記錄了fd與ANFD結構的映射關系。雖然,fd的申請和釋放操作會導致fd不一定是連續的,從而導致數組中出現空洞,但通過fd可以迅速獲取到相應的watcher列表,這也許是用空間換取時間的一個考量吧。另一方面,因為fd的釋放操作并不會發出通知,而系統分配fd總是采用可用的最小fd。所以如果一個fd在別處被釋放,這個fd則很有可能被分配給隨后打開的其他文件。而libev對這個過程是完全不知情的,所以它會傻傻的一直認為這個fd一直指向同一個文件,默默地服務著上面發生的事件。libev不負責管理fd的改變行為,而是把這個任務交給了外面,也就是說,如果外面fd發生了改變,需要調用ev_io_set或ev_io_init來重新設定fd與watcher的關系。
ev_io的插入
從前面的介紹我們知道,要通過libev來監聽fd上的事件,得要先插入一個ev_io到libev的loop中。ev_io的插入操作被封裝在ev_io_start函數中。毫無疑問,libev首先會根據fd找到對應的watcher列表,并將新watcher加入到列表中。接下來,會調用fd_change函數,將fd加入到loop->fdchanges中。fdchanges是一個簡單的數組,記錄的是當前注冊事件有發生改變的fd。到此為止,新ev_io的插入完成,上面的所有操作時間代價都是O(1)。fdchanges的作用在下一個小節中進行分析。
ev_io的選取
前面我們已經向libev的loop中插入了一個ev_io,那么libev是怎么把這個ev_io注冊到底層io并響應底層的io事件的呢? 要回答這個問題,我們得先回到上一篇文章。從ev_run流程圖中可以看到,ev_io的選取由fd_reify和backend_poll這兩個步驟來完成。
fd_reify函數的工作主要是遍歷fdchanges,將對應列表的watcher的events字段合并到ANFD結構的events字段。ANFD上如果新的events與原來監聽的events不一致,則表示在這個fd上監聽的事件集發生了變化,需要將fd和事件集合通過backend_modify注冊到底層的io庫。
在循環的后面,則會調用backend_poll來檢查fd上是否有注冊的事件發生。如果有事件發生,則通過fd_event函數,遍歷fd對應的watcher列表,比較每個watcher上的events字段與發生的事件event值,找出就緒的watcher添加到pendings中。
最后,這些pendings中的watcher會在循環結束前調用ev_invoke_pending來統一觸發。
ev_io的移除
ev_io的移除由ev_io_stop來完成。首先,會先檢查該watcher是否在pendings列表中,如果是,則先從pendings中刪除該watcher。pendings是一個數組,libev中的數組一般是以數組和元素數量來維護的。刪除數組中的一個元素,只要把數組末尾的元素替換掉被刪除的元素,并把元素數量減一就可以了,操作的時間復雜度是O(1) 。
接下來就是通過fd找到watcher列表,從中刪除這個watcher。這個操作需要遍歷列表找到待刪除的watcher,所以平均時間復雜度是O(n)。其中n是注冊在fd上的watcher數量,一般這個數量不會太大。
然后是把watcher的active標志位復位,并減少全局active的watcher計數。
最后是把fd加入到fdchanges中,因為移除一個watcher,可能會改變fd上感興趣的事件,所以要在下一輪循環中重新計算該fd上的事件集合。
ev_timer
ev_timer的管理
ev_timer watcher是主要負責處理超時事件的watcher。這類watcher被存儲在loop->timers中,它們的特點是,超時時間小的watcher會被先觸發。所以,timers其實是一個按觸發時間升序排序的優先隊列,底層的數據結構是一個用數組實現的二叉或四叉最小堆(關于堆的定義請google之)。ev_timer watcher的active字段,維護的其實是watcher在堆中的下標,通過它可以快速在堆中定位到watcher。
堆相關基本的堆操作有upheap和downheap。upheap操作是將一個節點上移到堆中合適的位置;downheap操作則剛好相反,將一個節點下移到堆中合適的位置。最小堆的特點是父節點的值比子節點的值都要小。通過這兩個操作,可以調整堆中節點的位置,以滿足最小堆的約束。它們的時間復雜度都是O(log(n))。有了這兩個操作,便可以構建出watcher結構的常用操作了。
-
獲取超時的watcher。因為timers是一個以觸發時間排序的最小堆,根部的watcher總是最先要觸發的watcher。所以這個操作的主要工作就是比較根部watcher的觸發時間,如果可以觸發,則加入pendings隊列。然后檢查該watcher是否是repeat的。如果是則更新下一次觸發時間,調用downheap操作將這個節點下移至合適的位置;否則直接刪除該watcher。
-
添加新的watcher。這無非就是一個入堆的操作。將新watcher添加到timers數組的末尾,再執行upheap操作,上升至合適的位置即可。
-
刪除watcher。將堆尾節點替換掉待刪除節點,再根據情況用upheap或downheap操作來調整替換后節點到合適的位置。
以上這些操作的時間代價都是O(log(n))。
timer的使用策略
在實際應用中,可能會出現頻繁使用大量timer的場景。比如:為每個請求設置一個超時時間,在指定時間內得不到響應,則報錯。如果為每一個請求創建一個watcher,則將產生大量不必要的空間和計算開銷。在libev的官方文檔中,提供了一個比較高效的方法。下面簡單介紹一下這種方法的思路。
可以使用一個雙向鏈表來維護超時時間相同的timer,這里的timer可以理解為定時器的記錄結構,比如:超時時間和超時的回調函數等。因為大家的超時時間是一樣的,所以新的timer進來后,肯定是添加到隊尾的。
然后分配一個ev_timer watcher專門來處理這個鏈表的超時工作。watcher的超時時間設置的是鏈表第一個元素的超時時間。當超時發生后,按鏈表順序觸發超時的timer。如果timer是重復的,可以重新計算超時時間并加入到鏈表尾部;否則直接刪除timer記錄即可。然后再從鏈表首部獲取下一次超時時間,重復上面的流程。
采用這種方案,只需要使用一個ev_timer watcher來處理相同timeout時間的timer。而timer的增刪操作最后其實就是鏈表的插入和刪除操作,所以操作的時間代價都是O(1)。而timeout時間相同的約束,主要是要保證鏈表里的元素都是有序的,插入操作都是發生在鏈表的尾端。如果要取消某一個timer,因為是雙向鏈表,也可以在O(1)時間內從鏈表內移除掉指定的節點。
ev_prepare, ev_check, ev_idle
從角色上來看,這三個類型的watcher其實都是事件循環的一個擴展點。通過這三個watcher,開發者可以在事件循環的一些特殊時刻獲得回調的機會。
-
ev_prepare 在事件循環發生阻塞前會被觸發。
-
ev_check 在事件循環阻塞結束后會被觸發。ev_check的觸發是按優先級劃分的??梢员WC,ev_check是同一個優先級上阻塞結束后最先被觸發的watcher。所以,如果要保證ev_check是最先被執行的,可以把它的優先級設成最高。
-
ev_idle 當沒有其他watcher被觸發時被觸發。ev_idle也是按優先級劃分的。它的語義是,在當前優先級以及更高的優先級上沒有watcher被觸發,那么它就會被觸發,無論之后在較低優先級上是否有其他watcher被觸發。
這三類watcher給外部的開發者提供了非常便利的擴展機制,在這個基礎上,開發者可以做很多有意思的事情,也對事件循環有了更多的控制權。具體到底能做些什么,做到什么程度,那就要看開發者們的想象力和創造力了:)
總結
ev_io和ev_timer應該是libev中使用的最多的watcher了,也是比較典型的watcher。從底層的實現上來看,處理得恰到好處,精明而干練,可謂獨具匠心。好了,拍作者馬屁的話就少說了,這一輪libev的代碼分析也到此先告一段落了,在這說說感受吧。
看libev的代碼,最大的障礙應該是非里面漫天飛舞的宏莫屬了。但把握了libev的大概結構后,知道哪些家伙長得比較像宏(比如:一些看似全局的變量),知道哪些宏要到什么地方找定義(比如:ev_vars.h,ev_wrap.h),事情就變得簡單了。再回頭想想,宏也是個不錯的選擇。第一,它是一個不錯的代碼解耦手段。上層代碼依賴于宏,而宏在不同的環境下可以綁定到不同的底層代碼,切換底層的代碼而不會影響到上層代碼。第二,它也是提高性能的途徑。宏的綁定在預處理階段完成,不會有額外的動態查找和函數調用開銷。第三,也可以偷偷懶,用短小的宏代替一大坨代碼,寫的人省力,看的人也舒服,正所謂他好我也好,一舉多得~
轉載于:https://www.cnblogs.com/Huayuan/archive/2013/05/03/3058580.html
總結
以上是生活随笔為你收集整理的libev源码分析--常用的watcher的全部內容,希望文章能夠幫你解決所遇到的問題。