libevent結構分析

 
查看文章
   
libevent結構分析
2008年07月19日 星期六 11:40

libevent 結構分析

    libevent是一個針對*nix的高級IO的庫(FreeBSD:kqueue, Linux:epoll, Solaris:/dev/poll)的封裝(雖然對於windows它也能工作,不過它封裝的不是iocp,所以這裏就不討論了)。現在我們看看 libevent的實現結構。

1、libevent使用的數據結構簡介   
   libevent中使用了tail queue、red black tree,現在簡單總結一下,免得我自己忘了。
   1、tail queue:
     它的基本特性和單向鏈表一樣,但是在它的head中增加了一個指向末尾的指針,所以它能夠直接在鏈表的尾部插入數據,但是它所付出的代價是:
     它實現的代碼量要比同算法的單向鏈表增加15%,而且運行效率上要增加約20%的開銷。

     
     現在先看一個例子(以下代碼只爲了說明tail queue的使用,不考慮錯誤);
     TAILQ_HEAD(my_tq, entry) head;
     //just for show
     struct my_tq *my_head;
    
     struct entry {
     ...
     TAILQ_ENTRY(entry) entry_tq;
     ...
     };
    
     my_head = malloc(sizeof(my_tq));
    
     TAILQ_INIT(my_head);
     TAILQ_INSERT_HEAD(my_head, p1 = malloc(sizeof(struct entry)), entry_tq);
     TAILQ_INSERT_TAIL(my_head, p2 = malloc(sizeof(struct entry)), entry_tq);
     TAILQ_INSERT_AFTER(my_head, p2, malloc(sizeof(struct entry)), entry_tq);
    
     while (my_head->tqh_first != NULL)
        TAILQ_REMOVE(my_head, my_head->tqh_first, entry_tq);

     tail queue的操作相關宏:
     TAILQ_HEAD(HEADNAME, TYPE) head --->定義串連TYPE類型的結構,同時將這種"新"類型的tail queue結構命名爲HEADNAME, 最後還定義了一個變量“head”。
     TAILQ_INIT(&head) --->初始化tail queue的兩個指針,對列的頭指針和隊列的尾指針
    
     TAILQ_ENTRY (TYPE) --->這個宏使用在TYPE結構體的定義內部,它定義維護結構體對象在tail queue中的位置所需要的指針。
    
     TAILQ_INSERT_HEAD/TAILQ_INSERT_TAIL/TAILQ_INSERT_AFTER/TAILQ_REMOVE,這些宏的作用比較明顯,需要注意的是它們都需要傳入head指針和TAILQ_ENTRY定義的標識符。
     (我覺得宏最強的地方和最有問題的地方就是它不進行類型檢查,最讓代碼閱讀者頭痛的就是條件編譯^_^)。
   
    2、reb black tree
   
      red black tree(rbt) 是一種近似的二叉平衡樹,它的特點如下:
      1、根節點是黑色的
      2、表示rbt節點結構的空指針都會指向一個空的節點(rchild, lchild, lparent),而且空節點是黑色的
      3、紅色節點的rchild/lchild/parent都是紅色節點
      4、從根節點到空節點的任一路徑所包含的黑色節點數是相同的
     
      rbt很重要的一個操作就是子樹旋轉,它是調整rbt結構的輔助操作。例如:右旋轉:
         |                      |             
        A                      B           這裏需要注意的是旋轉前B的右子節點y變成
       / /                                / /         了A的左子節點
      B    C   =======>     x    A
     / /                                     / /
    x    y                                     y    C
   
      rbt的插入算法:
      按照二叉排序樹的流程插入一個新的元素
      並把該節點設置爲紅色
                   |
        調整新的"樹",使它滿足rbt的條件
                   |
            如果新節點的父節點爲黑色,那麼
            調整完成
                   |
     如果父節點的顏色爲紅色,那麼進行以下的操作:
     假設:cn爲當前節點
           pn 爲當前節點的父節點
           pun 爲當前節點的父節點的兄弟節點
           cn 是pn的左子節點。
     1、如果pun是紅色,那麼將pn和pun都變成黑色,同時把cn設爲pn->lparent,重新進入迭代
     2、如果pun是黑色,而且cn是pn的右子節點,那麼將cn = pn,然後做一次左旋轉LR(cn)操作,這樣就
       可以把調整的樹轉換到左子樹上,這樣就把樹的結構轉換到第三種情況。
     3、如果pun是黑色,而且cn是pn的左子節點,那麼將pn設置爲黑色,並把pn->lparent設置爲紅色,
        同時進行右旋轉RR(pn->lparent)-->這時整個調整流程結束。
       
      rbt的刪除算法:
      按照二叉排序樹的刪除流程刪除一個元素
    如果刪除的節點是紅色的,那麼它不會破壞rbt的條件,如果刪除的節點是黑色的,那麼就需要對新的樹進行調整,使它滿足tbt的條件
      設rn 爲替代被刪除節點的節點(由於所有"邏輯上懸空"的指針都會指向"特殊的空節點",所以比如存在替代被刪除節點的替代節點),且它是rn->lparent的左子節點(對於右子節點的情況處理方式是對稱的)
        rcn 爲rn的兄弟節點
                  |
       如果cn是紅色,那麼只要把它變成黑色就滿足rbt條件
       如果cn是黑色,那麼就要根據rcn的顏色分別進行處理:
         1、rcn是紅色,那麼可以知道cn->lparent是黑色,rcn的兩個子節點都是黑色,那麼把cn->lparent和w的顏色
              互換,並進行左旋轉操作LR(x->parent),這樣cn就會有一個新的兄弟節點,而且它是黑色的。
         2、rcn是黑色,而且rcn的兩個子節點都是黑色,這樣只要把rcn設爲紅色,並把當前節點變成它的父節點               cn = cn->lparent 進行遞規迭代就可以了
         3、rcn是黑色, 而且左子節點rcn->lleft是紅色,右子節點rcn->lright是黑色, 那麼把rcn和rcn->lleft的顏色
               互換,並進行右旋轉操作RR(rcn),這樣就會轉換到第四中情況,而且cn也有了一個新的兄弟節點
         4、rcn是黑色,而且rcn的右子節點是紅色,那麼將rcn與rcn->parent的顏色互換,並進行左旋轉操作

              LR(rcn->lparent),
             這樣就把缺少的黑色節點轉移的原來的rcn->lright節點上,而且這個節點是紅色的,這時只要把它的顏色轉變成黑色就能完成rbt條件的修補。
     
      現在我們對於紅黑樹的一些算法都比較清楚了,下面看看與紅黑樹操作相關的幾個宏的使用
      RB_HEAD(my_rb_tree, rb_entry) rb_head;
     
      struct my_rb_tree * rb_tree;
     
      struct rb_entry {
         ...
         RB_ENTRY(rb_entry) rb_entry_list;
         ...
      };
     
      rb_tree = malloc(sizeof(struct my_rb_tree))
      RB_INIT(rb_tree);
      ...
     
      struct rb_entry * prb1 = NULL, *prb2 = NULL;
      ...
     
      RB_INSERT(my_rb_tree, rb_tree, prb1 = malloc(sizeof(struct rb_entry)))
      RB_INSERT(my_rb_tree, rb_tree, prb2 = malloc(sizeof(struct rb_entry)))
      ...
     
      struct rb_entry * min = RB_MIN(my_rb_tree, rb_tree);
      struct rb_entry * max = RB_MAX(my_rb_tree, rb_tree);
      assert(RB_EMPTY(rb_tree))
      ...
      RB_FOREACH(tmp_entry, my_rb_tree, rb_tree)
   custom_function(temp_entry);
  
   struct rb_entry * t_entry = NULL;
   RB_NEXT(my_rb_tree, rb_tree, t_entry);
   ...
   RB_REMOVE(my_rb_tree, rb_tree, prb1);
   ...
   RB_FIND(my_rb_tree, rb_tree, prb2)
  
   上面的這些操作都比較簡單,參數的含義也比較明顯,這裏就不多說了。
  
   需要注意的是rbt定義的輔助宏:
   RB_PROTOTYPE(my_rb_tree, rb_entry, rb_entry_list, compare):聲明一些操作函數,它的參數的含義是:
          my_rb_tree: 用戶定義的rbt的名稱, 它可用於後面的變量定義
          rb_entry:   rbt中保存的結構體類型
          rb_entry_list: rb_tree定義中RB_ENTRY定義的字段成員字段的名稱
          compare :   比較函數,用於在find算法和remove算法中查找目標元素,它的原型是:int compare(struct rb_entry * a, struct rb_entry* b)
   RB_GENERATE(my_rb_tree, rb_entry, rb_entry_list, compare):產生操作函數的代碼, 它的參數的含義和RB_PROTOTYPE的含義是一樣的。

2、libevent的處理流程
   現在我們看看libevent的基本處理流程;
   初始化libevent的運行環境
   event_init()
       |
   event_set(&event, filedescriptor, trigger_event, call_back, *parameter)-->初始化事件的結構
       |
   event_add(&event, timeinterval)-->增加查詢對列中的事件
       |
   event_dispatch()
  
   event_init
       |
   創建event_base結構,base = calloc(sizeof(struct event_base))
       |
   檢查系統是否支持clock_gettime, detect_monotic()
   獲取系統當前的時間(事實上libevent的工作都是基於定時模型,所以提取系統的時間是非常重要的)
   gettime(&base->event_tv)
      |
   初始化保存數據的紅黑樹和事件隊列
   RB_INIT(&base->timetree);
TAILQ_INIT(&base->eventqueue);
TAILQ_INIT(&base->sig.signalqueue);
     |

初始化IO系統,對於不同的平臺的初始化函數是不同的,

現在以freebsd(kqueue)爲例,跟蹤一下系統的操作流程
     |
kq_init(base)
     |--->初始化struct kqop結構 kqueueop = calloc(sizeof(struct kop))
     |            |
     |    創建kqueue描述符, kq = kqueue()
     |            |
     |    創建struct kevnet結構隊列,用於kevent函數時用於監測的隊列
     |    kqueueop->changes = malloc(NEVENT * sizeof(struct kevent))
     |    創建監測返回隊列
     |            |
     |    kqueueop->events = malloc(NEVENT * sizeof(struct kevnent))
     |            |
     |    測試系統是否真的支持kqueue
     |    kqueueop->changes[0].ident = -1;
   | kqueueop->changes[0].filter = EVFILT_READ;
     | kqueueop->changes[0].flags = EV_ADD;
     |          |
     | 當下列情況發生時,就表示該系統並非真的支持kqueue工作方式,一般只有在
     | Mac OS X平臺上纔會有這種情況發生
     | kevent(kq, kqueueop->changes, 1, kqueueop->events, NEVENT, NULL) != 1 ||
     |         kqueueop->events[0].ident != -1 || kqueueop->events[0].flags != EV_ERROR
     |
創建優先級隊列,它是用於保存被激活的事件
event_base_priority_init(base, 1)
     |       |-->如果當前有事件是激活的,那麼就直接返回-1,否則檢查需要創建的隊列數是否和目前的
     |           隊列數一樣,如果不一樣就釋放當前的資源,然後重新分配新的資源。
     |
設置全局變量current_base,它在很多函數中是充當默認參數的角色
current_base = base;
return (base);

event_set(struct event * ev, int fd, short events, void(*call_back)(int, short, void*), void *arg)
      |
這個函數比較簡單,只是簡單地初始化事件結構struct event,這裏需要注意的是這裏使用了在event_init初始化的
全局變量current_base變量,而且event_set中把事件的優先級設置爲中等的值

event_add(struct event* ev, struct timeval *tv)
和很多有超時設置的異步io函數一樣,event_add的操時參數tv是可選的,在libevent中(以freebsd平臺爲例)對於
IO的監控其實是分了兩個機制:
   1、調用kqueue/kevent直接監控IO
   2、對於設置了超時的IO,超時置是通過內建的rbt來保存的,對於"阻塞的監控"(反覆調用kevent進行操作,也就是
         開發者調用了event_dispatch後,直到沒有需要監控的IO才返回),一次調用kevnet的超時時間就是當前時間

         到下一個IO超時的時間
        
清楚了libevent的處理方式,對於event_add的操作就比較清楚了:
如果超時參數tv非空
|----->清除與該事件的相關狀態信息(如果該事件已經在超時對列中,那麼把該事件從操時隊列中刪除)
|              |                 (如果該時間由於超時原因被激活,那麼把事件從激活對列中刪除)
|              |                  
|       將超時時間調整爲絕對時間,並把事件加入到rbt中

檢查事件的結果標誌,將事件分別加入到kqueue的監控隊列、信號隊列中(這些對列是libevent維護的數據結構,而不是kqueue)

event_queue_insert(base, ev, EVLIST_INSERTED)/event_queue_insert(base, ev, EVLIST_SIGNAL)
     |
將事件加入到kqueue監控隊列
evsel->add(evbase, ev)--->kq_add(void * arg-->struct kqop*, struct event* )
                              |
             總的來說kq_add函數是根據struct event的參數初始化kevent結構並加入到系統的監測隊列中
             對於信號-->kev.filter = EVFILT_SIGNAL
                "可讀"-->kev.filter = EVFILT_READ
                "可寫"-->kev.filter = EVFILT_WRITE
             需要注意的是,讀寫是可以同時加入kqueue的,但是信號是單獨加入kqueue的,這是因爲信號的fd並
             不是一個有效的IO描述符,kqueue的信號處理和signal/sigaction機制是兼容的,kqueue記錄所有的
             signal處理,即使是標誌了SIG_IGN的信號,但是它的優先級比sigal/sigaction低。
             (kev.fflags = NOTE_EOF是爲了與select/poll的模式兼容,在讀到eof的時候激活事件,對於socket
               而言,就是在讀完了緩衝區的內容,並且對方已經關閉了寫端後會激活事件)
                              |
                    把監測的IO加入到kqueue中
                       kq_insert(kqop, &kev)
              kq_add簡單的把kev的數據保存到kqop中(當kqop中保存監聽事件和激活事件的數組已經滿時,進行了
                2倍的容量重新分配)

event_dispatch()
     |-->event_loop(0)
           |-->event_base_loop(current_base, flags)-->這裏使用了全局變量current_base作爲調度對象,所以
                      |                               它不是線程安全的
      event_base_loop(struct event_base *base, int flags)
      event_base_loop結合了線程安全和非線程安全的接口
     檢查base需要監聽的信號對列base->sig.signalqueue
     設置全局struct event_base * evsignal_base = base
    (信號是面向進程的,用一個全局變量來標識信號的處理也很合理,不過開發者需要保證多線程調用event_base中的信號隊列只有一個是非空的)
                |
      重新設置監控對列中的數據結構,(freebsd中就是kevent,不過它不需要設置,所以kq_recalc只是簡單地返回0)
      檢查base->event_gotterm,事件處理函數可以通過修改它的值控制循環的結束(這個也可以用在多線程環境中來終止一個監聽線程的工 作)。檢查全局變量event_gotsig,這個是全局的接口,主要是方便在單線程環境境下簡化編程工作,與event_gotsig配合的回調函數是 event_sigcb
                   |        
調整超時rbt的絕對時間 timeout_correct,這裏調整是因爲對於沒有提供clock_gettime(CLOCK_MONOTONIC)的系統,libevent是 通過gettimeofday()來獲得系統當前的時間的,而gettimeofday會受到一些外部因素的影響(例如:ntpupdate等)。但是 timeout_correct的算法只是當系統時間滯後與前一次檢測時間 時纔會進行超時事件的時間調整,也就是說:
        libevent保證超時時間的激活不會大於event_add設定的超時時間,當由於某些原因造成當前系統時間滯後時,
libevent 會對每個事件的喚醒時間進行調整,這是等待時間是不變的,但是如果系統的時間被調整爲超前於實際的時間,那麼事件的等待時間就會變少。當然這些情況只對於 使用gettimeofday的平臺適用,對於提供clock_gettime(CLOCK_MONOTONIC)調用的平臺,事件等待的始終是 event_add設定的超時時間。(timeout_correct中修改的正好是rbt的key,不過它是順序進行相同的修正,所以並沒有破壞rbt 的結構)
                |
      計算最小的內核等待時間(調用kevent的超時時間):
         如果沒有已經存在激活的事件(由於優先級的原因,有的激活的事件可能還留在低優先級的隊列中),或者非"阻塞"的循環等待模式那麼內核等待的時間爲零,否則就等待下一個超時值到來的時間
                |
         進入內核等待evsel->dispatch(base, evbase, tv_p)
      kq_dispatch(struct event_base *base, void *arg, struct timeval *tv)
                |
           如果設定了超時值,那麼就將struct timeval 結構轉化爲struct timespec結構
           調用內核檢測函數kevnet
           res = kevent(kqop->kq, changes, kqop->nchanges, events, kqop->nevents, ts_p);
                  |
           循環檢查返回的事件結構
           ev = (struct event *)events[i].udata;
           如果ev沒有設置(ev->ev_events & EV_PERSIST),那麼在libevent維護的事件隊列中刪除對應的事件
           event_del(ev);
                 |
           將激活的事件加入到對應的激活隊列中
           event_active(ev, which, ev->ev_events & EV_SIGNAL ? events[i].data : 1);
            |
     處理超時事件timeout_process
     timeout_process的處理比較簡單,它通過枚舉尋找rbt中已經超時的事件,並把該事件從超時事件隊列中
     刪除,同時把事件加入到激活對列中
            |
     處理激活隊列中的事件
     event_process_active(base);
     這裏有兩個地方需要注意的:
       1、event_process_active是根據優先級來調度已經激活的事件隊列的,而且一次只會處理一個非空的事件隊   列,如果高優先級(下標小的隊列)的隊列一直有激活的事件,那麼低優先級隊列就會一直被滯後。
        2、回調函數可以通過修改ev_pncalls指向的變量的值來中斷同一個事件的多次激活,而且可以通過全局變量
event_gotsig來中斷整個event_process_active調用。         
          
   對於libevent的基本處理流程我們已經比較清楚了。
   libevent中兼容了多個系統平臺的IO的特點,這裏就不展開了^_^。  

3、整體感覺
   1、libevent使用了很經典的通過結構體的函數指針來維護不同平臺的差異。
   2、libevent中通過rbt來維護超時事件的信息,在效率上有很可觀的表現,同時也考慮了時間的修正問題,對我來說很有啓發性^_^。

(本blog信息均爲原創,裝載請註明出處^_^)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章