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信息均为原创,装载请注明出处^_^)


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值