Libevent初探及其设计模式

libevent介绍

Libevent是一个用C语言编写的、轻量级的开源高性能事件通知库,主要有以下几个亮点:
1、事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;
2、源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 *BSD 和 Mac Os;
3、支持多种 I/O 多路复用技术, epoll、 poll、 dev/poll、 select 和 kqueue 等;
4、支持 I/O,定时器和信号等事件;注册事件优先级。Libevent 已经被广泛的应用,作为底层的网络库;

libevent源代码文件组织

1、前言

详细分析源代码之前,如果能对其代码文件的基本结构有个大概的认识和分类,对于代码的分析将是大有裨益的。本节内容不多,我想并不是说它不重要!

2、源代码组织结构

Libevent的源代码虽然都在一层文件夹下面,但是其代码分类还是相当清晰的,主要可分为头文件、内部使用的头文件、辅助功能函数、日志、libevent框架、对系统I/O多路复用机制的封装、信号管理、定时事件管理、缓冲区管理、基本数据结构和基于libevent的两个实用库等几个部分,有些部分可能就是一个源文件。
源代码中的test部分就不在我们关注的范畴了。
1)头文件
主要就是event.h:事件宏定义、接口函数声明,主要结构体event的声明;
2)内部头文件
xxx-internal.h:内部数据结构和函数,对外不可见,以达到信息隐藏的目的;
3)libevent框架
event.c:event整体框架的代码实现;
4)对系统I/O多路复用机制的封装
epoll.c:对epoll的封装;
select.c:对select的封装;
devpoll.c:对dev/poll的封装;
kqueue.c:对kqueue的封装;
5)定时事件管理
min-heap.h:其实就是一个以时间作为key的小根堆结构;
6)信号管理
signal.c:对信号事件的处理;
7)辅助功能函数
evutil.h 和evutil.c:一些辅助功能函数,包括创建socket pair和一些时间操作函数:加、减和比较等。
8)日志
log.h和log.c:log日志函数
9)缓冲区管理
evbuffer.c和buffer.c:libevent对缓冲区的封装;
10)基本数据结构
compat/sys下的两个源文件:queue.h是libevent基本数据结构的实现,包括链表,双向链表,队列等;_libevent_time.h:一些用于时间操作的结构体定义、函数和宏定义;
11)实用网络库
http和evdns:是基于libevent实现的http服务器和异步dns查询库;

Libevent事件处理流程

Libevent初探及其设计模式_第1张图片

基本使用场景和事件流程:
当应用程序向libevent 注册一个事件后,libevent 内部是怎么样进行处理的呢?下面的图就给出了这一基本流程。

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

Libevent设计模式

5大IO模型

1、 同步阻塞IO(Blocking IO)
即传统的IO模型。当用户进程向系统发起read操作时,首先需要在内核中数据准备和内核态到用户进程的数据拷贝。当两个步骤都完成后,才会返回read结果状态,才能执行后续的数据处理操作。这种read会阻塞程序,现在大部分都不使用这种模式了。

{
    read(socket, buffer);
    process(buffer);
}

2、 同步非阻塞IO(Non-blocking IO)
默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。当用户进程向系统发起read操作时,立即返回,但此时并没有读取到数据。用户线程需要不断地发起read请求,并根据返回的结果是否完成状态(EWOULDBLOCK),来确定是否完成read操作。

while(read(socket, buffer) == SUCCESS) {
    process(buffer);
}

在非阻塞式IO中,用户进程需要不断的主动询问数据准备好了没有,需要消耗过多的CPU 资源。
3、 IO多路复用(IO Multiplexing)
即经典的Reactor设计模式,有时也称为异步阻塞IO,Linux中的epoll都是这种模型。以下以select为例进行说明。使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

{
    select(socket);
    while(1) {
        ready_sockets = select();
        for(socket in ready_sockets) {
            if(can_read(socket)) {
                read(socket, buffer);
                process(buffer);
            }
        }
    }
}

4、 异步IO(Asynchronous IO)
即Proactor设计模式,也称为异步非阻塞IO。用户进程发起read操作之后,立刻就可以开始去做其它的事。而内核在接收到asynchronous read之后,内核会进行数据准备和数据拷贝至用户内存,当这两个步骤都完成后,内核会给用户进程发送一个signal,通知read操作完成。这一过程不会对用户进程产生任何block。

相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)
下面用一张图区别四种模型的区别:
Libevent初探及其设计模式_第2张图片

Reactor模式

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

下图描述了Reactor模式的框架,主要包括:事件源、框架部分(Reactor)、事件多路分发机制(event demultiplexing)、事件处理程序(event handler)。

Libevent初探及其设计模式_第3张图片

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

在libevent中有三种类型的事件:定时器事件(time event)、信号事件(signal event)和I/O事件。

2、事件多路分发机制(event demultiplexing)
需要使用底层提供的多路复用机制,如evport, select , poll, epoll, kqueue, devpoll. 用户进程首先在event demultiplexing上注册事件,采用合适的多路复用机制检测事件,当事件发生时,event demultiplexing发出通知“在已经注册的事件集中,一个或多个事件已经就绪“,程序收到通知后对事件进行处理。

1)libevent中对多路复用机制进行了封装,使得根据操作系统,可以选择最高效的IO机制。

2)事件注册:
首先,对event进行初始化,并将event与event_base(可以理解为事件库)关联起来,如下:

event_new(struct event_base base, evutil_socket_t fd, short events, void (*cb)(evutil_socket_t, short, void ), void *arg)
1
其中cb表示事件处理函数,也即回调函数,需要用户实现。

然后,将事件添加到事件库,此时event的状态为pending:
int event_add(struct event *ev, const struct timeval *tv);

3)事件触发:
在事件加入event_base后,选择合适的多路复用机制遍历事件队列,将状态为激活(active)的事件插入到激活队列中,从高到低优先级遍历激活event优先级数组。对于激活的event,调用event_queue_remove将之从激活队列中删除掉。然后再对这个event调用其回调函数。

你可能感兴趣的:(Libevent源代码分析)