初極狹,才通人。復行數十步,豁然開朗


因为上一个note的缘故,开始看libevent的源码,几天过去了,对于上一个note的问题有了新的认知(会在下一个note中说明),这里就讲讲什么豁然开朗。

一开始看的是这里的文档,并跟着写一些小例子,对libevent有了整体的认识后便在github上找了libevent的1.4.15版,开始阅读代码。

整个感受如标题一样,最后是豁然开朗。虽然源码只看了部分,但是libevent核心部分已经明白了,下面就来说说。

首先放出两个极其重要的结构

两个结构
//event-internal.h (libevent-1.4.15-stable)
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;
};

struct event_base {
	const struct eventop *evsel;
	void *evbase;
	int event_count;		/* counts number of total events */
	int event_count_active;	/* counts number of active events */

	int event_gotterm;		/* Set to terminate loop */
	int event_break;		/* Set to terminate loop immediately */

	/* active event management */
	struct event_list **activequeues;
	int nactivequeues;

	/* signal handling info */
	struct evsignal_info sig;

	struct event_list eventqueue;
	struct timeval event_tv;

	struct min_heap timeheap;

	struct timeval tv_cache;
};

先说struct eventop,看名字可以猜测是_event opearation_,包含了两个数据成员以及5个接口(相当于C++中的虚基类)。libevent包含的信号IO定时器都使用这个结构来管理,同时,为了跨平台,IO部分又有多种选择(poll,select,epoll,kqueue...),这就意味着这些实现都必须有统一的形式。
epoll来说,有一个单独的文件epoll.c(其他的实现也是单独的一个文件,比如:signal.c),这里面包含的操作都是static的,同时这些接口是通过epoll.c里面的全局的struct eventop暴露出来的。由于C没有namespace,这样封装挺好的。同理,其他的操作也都是通过这个结构体把具体实现暴露出来。
那么,怎么在这些实现里面选呢?在event.c这个文件中有如下代码:

#ifdef HAVE_EVENT_PORTS
extern const struct eventop evportops;
#endif
#ifdef HAVE_SELECT
extern const struct eventop selectops;
#endif
#ifdef HAVE_POLL
extern const struct eventop pollops;
#endif
#ifdef HAVE_EPOLL
extern const struct eventop epollops;
#endif
#ifdef HAVE_WORKING_KQUEUE
extern const struct eventop kqops;
#endif
#ifdef HAVE_DEVPOLL
extern const struct eventop devpollops;
#endif
#ifdef WIN32
extern const struct eventop win32ops;
#endif

/* In order of preference */
static const struct eventop *eventops[] = {
#ifdef HAVE_EVENT_PORTS
	&evportops,
#endif
#ifdef HAVE_WORKING_KQUEUE
	&kqops,
#endif
#ifdef HAVE_EPOLL
	&epollops,
#endif
#ifdef HAVE_DEVPOLL
	&devpollops,
#endif
#ifdef HAVE_POLL
	&pollops,
#endif
#ifdef HAVE_SELECT
	&selectops,
#endif
#ifdef WIN32
	&win32ops,
#endif
	NULL
};

又是一个全局变量static const struct eventop *eventops[],这样做的话,我们就可以选则需要的实现了。实现的第一个数据成员是该实现的名字,比如实现是epoll那么这个name就可能是"epoll",只要做一个循环就可以得到想要的实现了,挺好的。

先不讲具体实现,下面来看看第二个结构struct event_base讲讲它的作用。它是所有事件的集中管理,IO事件和信号使用的是链表struct event_list

// event.h
#ifdef _EVENT_DEFINED_TQENTRY
#undef TAILQ_ENTRY
struct event_list;
struct evkeyvalq;
#undef _EVENT_DEFINED_TQENTRY
#else
TAILQ_HEAD (event_list, event);
TAILQ_HEAD (evkeyvalq, evkeyval);
#endif /* _EVENT_DEFINED_TQENTRY */
//----------------------------------------------------
// queue.h
#define TAILQ_HEAD(name, type)						\
struct name {								\
	struct type *tqh_first;	/* first element */			\
	struct type **tqh_last;	/* addr of last next element */		\
}

而定时器用的是最小堆。
当事件发生后,从事件队列中取出一个事件标记为活动,并将其加入到活动队列。
下面说说其他成员:

  • eventop 统一接口的指针
  • evbase 这是一个void指针,对不同的实现指向不同的结构,比如epoll的实现中指向的是struct epollop(epoll.c)
  • sig 这是一个信号相关操作的封装,后面会再提到
  • event_tv 用来记录定时器的超时时间
  • timeheap 存放定时器超时时间的最小堆
  • tv_cache 保存上一次的超时时间,避免过多的系统调用(比如gettimeofday

事件循环

代码有好几十行,所以不贴出来,下面讲讲这个循环(event_base_loop)里面要做的事

  1. 首先判断是否注册有信号事件
  2. 进入循环后判断用户是否要求退出
  3. 判断是否收到了信号,如果搜到就调用注册的回调,很变态的是这个变量只在event.c而且没有一个地方将它置1
  4. 纠正时间,如果用户修改了系统时间的话(参考monotonic clock和steady clock)
  5. 更新时间
  6. 调用统一接口dispatch,这是一个阻塞操作,对IO和信号事件会阻塞到有IO事件发生, 对定时器事件会设置超时时间,比如,对epoll来说就是设置epoll_wait的第三个参数为定时器的超时时间。对于IO和信号,如果事件发生,就讲这个事件加入到活动事件列表
  7. 对于定时器事件,调用timeout_process判断是否真的超时,如果超时,同样将这个事件加入到活动事件列表
  8. 如果活动事件列表大小不为0,那么开始处理这些事件,处理完成后,设置退出标准为1

__注意:__这里的设计是非常糟的,用了好几个全局变量并且都没有加锁保护。

豁然开朗

如你所见,不管是信号还是IO还是定时器,在事件循环中只有一个dispatch调用。原因是:

  • __IO:__在multiplexer中正好使用打开的文件描述符添加到监控列表
  • 信号: 在结构体evsignal_info中有一对socket pair,一端用于添加到监控列表,另一端放在信号处理器中,用于产生IO事件来使multiplexer返回
  • __定时器:__传递文件描述符为-1,利用设置等待超时灵multiplexer返回

就这样,三个不同的东西就通过这么一个统一的接口结合在了一起。挺好的!


2016-07-11 更新

最后还要吐槽一下,用了太多的全局变量。拿信号来说,不用全局变量就没办法产生socketpair的可读事件,也是醉了,不过当前版本的signal.c中已经把原来的结构体换成一个int了。

然后又去看了看boost.asio的信号相关的源码,在signal_set_service.ipp中发现也是使用了一个全局变量

namespace asio {
namespace detail {

struct signal_state
{
  // Mutex used for protecting global state.
  static_mutex mutex_;

  // The read end of the pipe used for signal notifications.
  int read_descriptor_;

  // The write end of the pipe used for signal notifications.
  int write_descriptor_;

  // Whether the signal state has been prepared for a fork.
  bool fork_prepared_;

  // The head of a linked list of all signal_set_service instances.
  class signal_set_service* service_list_;

  // A count of the number of objects that are registered for each signal.
  std::size_t registration_count_[max_signal_number];
};

signal_state* get_signal_state()
{
  static signal_state state = {
    ASIO_STATIC_MUTEX_INIT, -1, -1, false, 0, { 0 } };
  return &state;
}
//.....
}
}

不同的是asio这里面至少有一个互斥量啊(还用了pthread_sigmask,链接的时候要加上lpthread)!!还有一点不同的是,asio里面用的是pipe



转载请注明:Serenity » 初極狹,才通人。復行數十步,豁然開朗