libevent源码剖析

你的项目为什么使用libevent网络库呢??咳咳~~本篇对libevent进行简单剖析~

首先说几点libevent的显著亮点:
**1、事件驱动,高性能;
2、轻量级,专注于网络;不如ACE那么臃肿庞大;
3、源代码相当精炼、易读;
4、跨平台,支持windows、linux、*BSD和Mac Os;
5、支持多种I/O多路复用技术,epoll、poll、dev/poll、select和kqueue等;
6、支持I/O,定时器和信号等事件;
7、注册事件优先级**

一、Reactor的事件处理机制

(1)Reactor释义“反应堆”,是一种事件驱动机制。

普通函数调用:应用程序主动的调用某个API完成处理。
Reactor:应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口(回调函数)。

使用libevent也是向libevent框架注册相应的事件和回调函数,当这些事件发生时,libevent会调用这些回调函数处理相应的事件(I//O读写、定时和信号)

(2)Reactor模式的优点
Reactor模式是编写高性能网络服务器的必备技术之一,它具有如下的优点:
1)响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
4)可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;

(3)Reactor模式框架
使用Reactor模型,必备的几个组件:事件源、Reactor框架、多路复用机制和事件处理程序,先来看看Reactor模型的整体框架,接下来再对每个组件做逐一说明。
libevent源码剖析_第1张图片

1) 事件源
Linux上是文件描述符,Windows上就是Socket或者Handle了,这里统一称为“句柄集”;程序在指定的句柄上注册关心的事件,比如I/O事件。

2) event demultiplexer——事件多路分发机制
由操作系统提供的I/O多路复用机制,比如select和epoll。
程序首先将其关心的句柄(事件源)及其事件注册到event demultiplexer上;
当有事件到达时,event demultiplexer会发出通知“在已经注册的句柄集中,一个或多个句柄的事件已经就绪”;
程序收到通知后,就可以在非阻塞的情况下对事件进行处理了。
对应到libevent中,依然是select、poll、epoll等,但是libevent使用结构体eventop进行了封装,以统一的接口来支持这些I/O多路复用机制,达到了对外隐藏底层系统机制的目的。

3) Reactor——反应器
Reactor,是事件管理的接口,内部使用event demultiplexer注册、注销事件;并运行事件循环,当有事件进入“就绪”状态时,调用注册事件的回调函数处理事件。
对应到libevent中,就是event_base结构体。
一个典型的Reactor声明方式

4) Event Handler——事件处理程序
事件处理程序提供了一组接口,每个接口对应了一种类型的事件,供Reactor在相应的事件发生时调用,执行相应的事件处理。通常它会绑定一个有效的句柄。
对应到libevent中,就是event结构体。

(4)Reactor事件处理流程
libevent源码剖析_第2张图片

二、libevent的接口函数
1)首先初始化libevent库,并保存返回的指针
struct event_base * base = event_init();
实际上这一步相当于初始化一个Reactor实例;在初始化libevent后,就可以注册事件了。

2)初始化事件event,设置回调函数和关注的事件
evtimer_set(&ev, timer_cb, NULL);
事实上这等价于调用event_set(&ev, -1, 0, timer_cb, NULL);
event_set的函数原型是:
void event_set(struct event ev, int fd, short event, void (*cb)(int, short, void ), void *arg)
ev:执行要初始化的event对象;
fd:该event绑定的“句柄”,对于信号事件,它就是关注的信号;
event:在该fd上关注的事件类型,它可以是EV_READ, EV_WRITE, EV_SIGNAL;
cb:这是一个函数指针,当fd上的事件event发生时,调用该函数执行处理,它有三个参数,调用时由event_base负责传入,按顺序,实际上就是event_set时的fd, event和arg;
arg:传递给cb函数指针的参数;
由于定时事件不需要fd,并且定时事件是根据添加时(event_add)的超时值设定的,因此这里event也不需要设置。
这一步相当于初始化一个event handler,在libevent中事件类型保存在event结构体中。
注意:libevent并不会管理event事件集合,这需要应用程序自行管理;

3)设置event从属的event_base
event_base_set(base, &ev);
这一步相当于指明event要注册到哪个event_base实例上;

4)是正式的添加事件的时候了
event_add(&ev, timeout);
基本信息都已设置完成,只要简单的调用event_add()函数即可完成,其中timeout是定时值;
这一步相当于调用Reactor::register_handler()函数注册事件。

5)程序进入无限循环,等待就绪事件并执行事件处理
event_base_dispatch(base);

当应用程序向libevent注册一个事件后,libevent内部是怎么样进行处理的呢?下面的图就给出了这一基本流程。
libevent源码剖析_第3张图片

1) 首先应用程序准备并初始化event,设置好事件类型和回调函数;这对应于前面第步骤2和3;
2) 向libevent添加该事件event。对于定时事件,libevent使用一个小根堆管理,key为超时时间;对于Signal和I/O事件,libevent将其放入到等待链表(wait list)中,这是一个双向链表结构;
3) 程序调用event_base_dispatch()系列函数进入无限循环,等待事件,以select()函数为例;每次循环前libevent会检查定时事件的最小超时时间tv,根据tv设置select()的最大等待时间,以便于后面及时处理超时事件;
当select()返回后,首先检查超时事件,然后检查I/O事件;
Libevent将所有的就绪事件,放入到激活链表中;
然后对激活链表中的事件,调用事件的回调函数执行事件处理;

三、libevent的核心-event

1.  struct event {  
//该I/O事件在链表中的位置;称此链表为“已注册事件链表”;
2.   TAILQ_ENTRY (event) ev_next;  
    //libevent将所有的激活事件放入到链表active list中,然后遍历active list执行调度,ev_active_next就指明了event在active list中的位置;
3.   TAILQ_ENTRY (event) ev_active_next;  
//signal事件在signal事件链表中的位置;
4.   TAILQ_ENTRY (event) ev_signal_next;  
5.   unsigned int min_heap_idx; //libevent使用小根堆来管理定时事件
6.   struct event_base *ev_base; //该事件所属的反应堆实例
7.   int ev_fd; //对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号; 
8.   short ev_events;//event关注的事件类型
    /*
    I/O事件: EV_WRITE和EV_READ
    定时事件:EV_TIMEOUT
    信号:    EV_SIGNAL
    辅助选项:EV_PERSIST,表明是一个永久事件
    */
9.   short ev_ncalls;  
10.  short *ev_pncalls; /* Allows deletes in callback */  
11.  struct timeval ev_timeout;  
12.  int ev_pri;  /* smaller numbers are higher priority */  
13.  void (*ev_callback)(int, short, void *arg); //event的回调函数 
14.  void *ev_arg;  
15.  int ev_res;  /* result passed to event callback */  
16.  int ev_flags;  
17. };  

libevent对event的管理方法,可以参见下面的示意图:

libevent源码剖析_第4张图片
**每次当有事件event转变为就绪状态时,libevent就会把它移入到active event list[priority]中,其中priority是event的优先级;
接着libevent会根据自己的调度策略选择就绪事件,调用其cb_callback()函数执行事件处理;并根据就绪的句柄和事件类型填充cb_callback函数的参数。**

四、事件处理框架-event_base

1.  struct event_base {  
2.   const struct eventop *evsel;  
3.   void *evbase;   
4.   int event_count;  /* counts number of total events */  
5.   int event_count_active; /* counts number of active events */  
6.   int event_gotterm;  /* Set to terminate loop */  
7.   int event_break;  /* Set to terminate loop immediately */  
8.   /* active event management */  
9.   struct event_list **activequeues;//其中的元素activequeues[priority]是一个链表,链表的每个节点指向一个优先级为priority的就绪事件event。   
10.  int nactivequeues;  
11.  /* signal handling info */  
12.  struct evsignal_info sig;  
13.  struct event_list eventqueue;  //保存了所有的注册事件event的指针。
14.  struct timeval event_tv;  
15.  struct min_heap timeheap; //管理定时事件的小根堆 
16.  struct timeval tv_cache;  
17. };  

evsel指向了全局变量static const struct eventop *eventops[]中的一个;
前面也说过,libevent将系统提供的I/O demultiplex机制统一封装成了eventop结构;因此eventops[]包含了select、poll、kequeue和epoll等等其中的若干个全局实例对象。
evbase实际上是一个eventop实例对象;

struct eventop {
 const char *name;
 void *(*init)(struct event_base *); // 初始化
 int (*add)(void *, struct event *); // 注册事件
 int (*del)(void *, struct event *); // 删除事件
 int (*dispatch)(struct event_base *, void *, struct timeval *); // 事件分发
 void (*dealloc)(struct event_base *, void *); // 注销,释放资源
 /* set if we need to reinitialize the event base */
 int need_reinit;
};

五、Libevent中对应的接口函数

1.  int  event_add(struct event *ev, const struct timeval *timeout);  
2.  int  event_del(struct event *ev);  
3.  int  event_base_loop(struct event_base *base, int loops);  
4.  void event_active(struct event *event, int res, short events);  
5.  void event_process_active(struct event_base *base); 

(1)注册事件

1.  int event_add(struct event *ev, const struct timeval *tv)  
2.  {  
3.   struct event_base *base = ev->ev_base; // 要注册到的event_base  
4.   const struct eventop *evsel = base->evsel;  
5.   void *evbase = base->evbase; // base使用的系统I/O策略  
6.   // 新的timer事件,调用timer heap接口在堆上预留一个位置  
7.   // 注:这样能保证该操作的原子性:  
8.   // 向系统I/O机制注册可能会失败,而当在堆上预留成功后,  
9.   // 定时事件的添加将肯定不会失败;  
10.  // 而预留位置的可能结果是堆扩充,但是内部元素并不会改变  
11.  if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {  
12.   if (min_heap_reserve(&base->timeheap,  
13.    1 + min_heap_size(&base->timeheap)) == -1)  
14.    return (-1);  /* ENOMEM == errno */  
15.  }  
16.  // 如果事件ev不在已注册或者激活链表中,则调用evbase注册事件  
17.  if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&  
18.   !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) {  
19.    res = evsel->add(evbase, ev);  
20.    if (res != -1) // 注册成功,插入event到已注册链表中  
21.     event_queue_insert(base, ev, EVLIST_INSERTED);  
22.  }  
23.  // 准备添加定时事件  
24.  if (res != -1 && tv != NULL) {  
25.   struct timeval now;  
26.   // EVLIST_TIMEOUT表明event已经在定时器堆中了,删除旧的  
27.   if (ev->ev_flags & EVLIST_TIMEOUT)  
28.    event_queue_remove(base, ev, EVLIST_TIMEOUT);  
29.   // 如果事件已经是就绪状态则从激活链表中删除  
30.   if ((ev->ev_flags & EVLIST_ACTIVE) &&  
31.    (ev->ev_res & EV_TIMEOUT)) {  
32.     // 将ev_callback调用次数设置为0  
33.     if (ev->ev_ncalls && ev->ev_pncalls) {  
34.      *ev->ev_pncalls = 0;  
35.     }  
36.     event_queue_remove(base, ev, EVLIST_ACTIVE);  
37.   }  
38.   // 计算时间,并插入到timer小根堆中  
39.   gettime(base, &now);  
40.   evutil_timeradd(&now, tv, &ev->ev_timeout);  
41.   event_queue_insert(base, ev, EVLIST_TIMEOUT);  
42.  }  
43.  return (res);  
44. }  
45.    
46. event_queue_insert()负责将事件插入到对应的链表中,下面是程序代码;  
47. event_queue_remove()负责将事件从对应的链表中删除,这里就不再重复贴代码了;  
48. void event_queue_insert(struct event_base *base, struct event *ev, int queue)  
49. {  
50.  // ev可能已经在激活列表中了,避免重复插入  
51.  if (ev->ev_flags & queue) {  
52.   if (queue & EVLIST_ACTIVE)  
53.    return;  
54.  }  
55.  // ...  
56.  ev->ev_flags |= queue; // 记录queue标记  
57.  switch (queue) {  
58.  case EVLIST_INSERTED: // I/O或Signal事件,加入已注册事件链表  
59.   TAILQ_INSERT_TAIL(&base->eventqueue, ev, ev_next);  
60.   break;  
61.  case EVLIST_ACTIVE: // 就绪事件,加入激活链表  
62.   base->event_count_active++;  
63.   TAILQ_INSERT_TAIL(base->activequeues[ev->ev_pri], ev, ev_active_next);  
64.   break;  
65.  case EVLIST_TIMEOUT: // 定时事件,加入堆  
66.   min_heap_push(&base->timeheap, ev);  
67.   break;  
68.  }  
69. }  

(2)删除事件

1.  int event_del(struct event *ev)  
2.  {  
3.   struct event_base *base;  
4.   const struct eventop *evsel;  
5.   void *evbase;  
6.   // ev_base为NULL,表明ev没有被注册  
7.   if (ev->ev_base == NULL)  
8.    return (-1);  
9.   // 取得ev注册的event_base和eventop指针  
10.  base = ev->ev_base;  
11.  evsel = base->evsel;  
12.  evbase = base->evbase;  
13.  // 将ev_callback调用次数设置为  
14.  if (ev->ev_ncalls && ev->ev_pncalls) {  
15.   *ev->ev_pncalls = 0;  
16.  }  
17.    
18.  // 从对应的链表中删除  
19.  if (ev->ev_flags & EVLIST_TIMEOUT)  
20.   event_queue_remove(base, ev, EVLIST_TIMEOUT);  
21.  if (ev->ev_flags & EVLIST_ACTIVE)  
22.   event_queue_remove(base, ev, EVLIST_ACTIVE);  
23.  if (ev->ev_flags & EVLIST_INSERTED) {  
24.   event_queue_remove(base, ev, EVLIST_INSERTED);  
25.   // EVLIST_INSERTED表明是I/O或者Signal事件,  
26.   // 需要调用I/O demultiplexer注销事件  
27.   return (evsel->del(evbase, ev));  
28.  }  
29.  return (0);  
30. }  

(3)事件处理主循环

libevent源码剖析_第5张图片

1.  int event_base_loop(struct event_base *base, int flags)  
2.  {  
3.      const struct eventop *evsel = base->evsel;  
4.      void *evbase = base->evbase;  
5.      struct timeval tv;  
6.      struct timeval *tv_p;  
7.      int res, done;  
8.      // 清空时间缓存  
9.      base->tv_cache.tv_sec = 0;  
10.     // evsignal_base是全局变量,在处理signal时,用于指名signal所属的event_base实例  
11.     if (base->sig.ev_signal_added)  
12.         evsignal_base = base;  
13.     done = 0;  
14.     while (!done) { // 事件主循环  
15.         // 查看是否需要跳出循环,程序可以调用event_loopexit_cb()设置event_gotterm标记  
16.         // 调用event_base_loopbreak()设置event_break标记  
17.         if (base->event_gotterm) {  
18.             base->event_gotterm = 0;  
19.             break;  
20.         }  
21.         if (base->event_break) {  
22.             base->event_break = 0;  
23.             break;  
24.         }  
25.         // 校正系统时间,如果系统使用的是非MONOTONIC时间,用户可能会向后调整了系统时间  
26.         // 在timeout_correct函数里,比较last wait time和当前时间,如果当前时间< last wait time  
27.         // 表明时间有问题,这是需要更新timer_heap中所有定时事件的超时时间。  
28.         timeout_correct(base, &tv);  
29.      
30.         // 根据timer heap中事件的最小超时时间,计算系统I/O demultiplexer的最大等待时间  
31.         tv_p = &tv;  
32.         if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {  
33.             timeout_next(base, &tv_p);  
34.         } else {  
35.             // 依然有未处理的就绪时间,就让I/O demultiplexer立即返回,不必等待  
36.             // 下面会提到,在libevent中,低优先级的就绪事件可能不能立即被处理  
37.             evutil_timerclear(&tv);  
38.         }  
39.         // 如果当前没有注册事件,就退出  
40.         if (!event_haveevents(base)) {  
41.             event_debug(("%s: no events registered.", __func__));  
42.             return (1);  
43.         }  
44.         // 更新last wait time,并清空time cache  
45.         gettime(base, &base->event_tv);  
46.         base->tv_cache.tv_sec = 0;  
47.         // 调用系统I/O demultiplexer等待就绪I/O events,可能是epoll_wait,或者select等;  
48.         // 在evsel->dispatch()中,会把就绪signal event、I/O event插入到激活链表中  
49.         res = evsel->dispatch(base, evbase, tv_p);  
50.         if (res == -1)  
51.             return (-1);  
52.         // 将time cache赋值为当前系统时间  
53.         gettime(base, &base->tv_cache);  
54.         // 检查heap中的timer events,将就绪的timer event从heap上删除,并插入到激活链表中  
55.         timeout_process(base);  
56.         // 调用event_process_active()处理激活链表中的就绪event,调用其回调函数执行事件处理  
57.         // 该函数会寻找最高优先级(priority值越小优先级越高)的激活事件链表,  
58.         // 然后处理链表中的所有就绪事件;  
59.         // 因此低优先级的就绪事件可能得不到及时处理;  
60.         if (base->event_count_active) {  
61.             event_process_active(base);  
62.             if (!base->event_count_active && (flags & EVLOOP_ONCE))  
63.                 done = 1;  
64.         } else if (flags & EVLOOP_NONBLOCK)  
65.             done = 1;  
66.     }  
67.     // 循环结束,清空时间缓存  
68.     base->tv_cache.tv_sec = 0;  
69.     event_debug(("%s: asked to terminate loop.", __func__));  
70.     return (0);  
71. }  

六、I/O和Timer事件的统一
**首先将Timer事件融合到系统I/O多路复用机制中,因为系统的I/O机制像select()和epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有I/O事件发生,它们也保证能在timeout时间内返回。
那么根据所有Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活所有就绪的Timer事件就可以了,这样就能将Timer事件完美的融合到系统的I/O机制中了。
堆是一种经典的数据结构,向堆中插入、删除元素时间复杂度都是O(lgN),N为堆中元素的个数,而获取最小key值(小根堆)的复杂度为O(1);因此变成了管理Timer事件的绝佳人选(当然是非唯一的),libevent就是采用的堆结构。**

具体的代码在源文件event.c的event_base_loop()中

 if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {  
2.            // 根据Timer事件计算evsel->dispatch的最大等待时间  
3.      timeout_next(base, &tv_p);  
4.  } else {   
5.            // 如果还有活动事件,就不要等待,让evsel->dispatch立即返回  
6.      evutil_timerclear(&tv);  
7.  }  
8.  // ...  
9.        // 调用select() or epoll_wait() 等待就绪I/O事件  
10. res = evsel->dispatch(base, evbase, tv_p);  
11.       // ...  
12.       // 处理超时事件,将超时事件插入到激活链表中  
13.       timeout_process(base);  

timeout_next()函数根据堆中具有最小超时值的事件和当前时间来计算等待时间,下面看看代码:

1.  static int timeout_next(struct event_base *base, struct timeval **tv_p)  
2.  {  
3.      struct timeval now;  
4.      struct event *ev;  
5.      struct timeval *tv = *tv_p;  
6.      // 堆的首元素具有最小的超时值  
7.      if ((ev = min_heap_top(&base->timeheap)) == NULL) {  
8.          // 如果没有定时事件,将等待时间设置为NULL,表示一直阻塞直到有I/O事件发生  
9.          *tv_p = NULL;  
10.         return (0);  
11.     }  
12.     // 取得当前时间  
13.     gettime(base, &now);  
14.     // 如果超时时间<=当前值,不能等待,需要立即返回  
15.     if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {  
16.         evutil_timerclear(tv);  
17.         return (0);  
18.     }  
19.     // 计算等待的时间=当前时间-最小的超时时间  
20.     evutil_timersub(&ev->ev_timeout, &now, tv);  
21.     return (0);  
22. }  

普通的堆管理代码:

1.  Heap[size++] = new; // 先放到数组末尾,元素个数+1  
2.  // 下面就是shift_up()的代码逻辑,不断的将new向上调整  
3.  _child = size;  
4.  while(_child>0) // 循环  
5.  {  
6.     _parent = (_child-1)/2; // 计算parent  
7.     if(Heap[_parent].key < Heap[_child].key)  
8.        break; // 调整结束,跳出循环  
9.     swap(_parent, _child); // 交换parent和child  
10. }  

而libevent的heap代码对这一过程做了优化,在插入新元素时,只是为新元素预留了一个位置hole(初始时hole位于数组尾部),但并不立刻将新元素插入到hole上,而是不断向上调整hole的值,将父节点向下调整,最后确认hole就是新元素的所在位置时,才会真正的将新元素插入到hole上,因此在调整过程中就比上面的代码少了一次赋值的操作

1.  // 下面就是shift_up()的代码逻辑,不断的将new的“预留位置”向上调整  
2.  _hole = size; // _hole就是为new预留的位置,但并不立刻将new放上  
3.  while(_hole>0) // 循环  
4.  {  
5.      _parent = (_hole-1)/2; // 计算parent  
6.      if(Heap[_parent].key < new.key)  
7.          break; // 调整结束,跳出循环  
8.      Heap[_hole] = Heap[_parent]; // 将parent向下调整  
9.      _hole = _parent; // 将_hole调整到_parent  
10. }  
11. Heap[_hole] = new; // 调整结束,将new插入到_hole指示的位置  
12. size++; // 元素个数+1  

由于每次调整都少做一次赋值操作,在调整路径比较长时,调整效率会比第一种有所提高

七、I/O和Signal事件的统一

在libevent中这是通过socket pair完成的, Socket pair就是一个socket对,包含两个socket,一个读socket,一个写socket。工作方式如下图所示:
libevent源码剖析_第6张图片
如何创建socket pair?
libevent源码剖析_第7张图片
Socket pair创建好了,可是libevent的事件主循环还是不知道Signal是否发生了啊,看来我们还差了最后一步,那就是:为socket pair的读socket在libevent的event_base实例上注册一个persist的读事件。
这样当向写socket写入数据时,读socket就会得到通知,触发读事件,从而event_base就能相应的得到通知了。
前面提到过,Libevent会在事件主循环中检查标记,来确定是否有触发的signal,如果标记被设置就处理这些signal,这段代码在各个具体的I/O机制中,以Epoll为例,在epoll_dispatch()函数中,代码片段如下:

1.  res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);  
2.  if (res == -1) {  
3.      if (errno != EINTR) {  
4.          event_warn("epoll_wait");  
5.          return (-1);  
6.      }  
7.      evsignal_process(base);// 处理signal事件  
8.      return (0);  
9.  } else if (base->sig.evsignal_caught) {  
10.     evsignal_process(base);// 处理signal事件  
11. }  

libevent源码剖析_第8张图片
Libevent中Signal事件的管理是通过结构体evsignal_info完成的,结构体位于evsignal.h文件中,定义如下:

1.  struct evsignal_info {  
2.      struct event ev_signal; //为socket pair的读socket向event_base注册读事件时使用的event结构体; 
3.      int ev_signal_pair[2];  
4.      int ev_signal_added; //记录ev_signal事件是否已经注册了 
5.      volatile sig_atomic_t evsignal_caught; //是否有信号发生的标记;是volatile类型,因为它会在另外的线程中被修改; 
6.      struct event_list evsigevents[NSIG]; //evsigevents[signo]表示注册到信号signo的事件链表; 
7.      sig_atomic_t evsigcaught[NSIG];  //记录信号signo被触发的次数
8.  #ifdef HAVE_SIGACTION  
9.      struct sigaction **sh_old; //原来的signal处理函数指针 
10. #else  
11.     ev_sighandler_t **sh_old;  
12. #endif  
13.     int sh_old_max;  
14. };  

evsignal_info的初始化包括,创建socket pair,设置ev_signal事件(但并没有注册,而是等到有信号注册时才检查并注册),并将所有标记置零,初始化信号的注册事件链表指针等。

libevent的巧妙设计确实值得我们好好学习啊~_~_~_~

你可能感兴趣的:(网络)