Linux高性能I/O框架库Libevent介绍

猿友 2020-08-13 14:48:45 浏览数 (10562)
反馈

这篇文章主要讲一下Libevent库的内容,顺便对I/O库整体做个介绍。

Linux服务器程序必须处理的三类事件:

  • I/O事件
  • 信号
  • 定时事件

在处理这三类事件时我们通常需要考虑如下三个问题:

  • 统一事件源。很明显,统一处理这三类事件既能使代码简单易懂,又能避免一些潜在的逻辑错误。
  • 可移植性。不同的操作系统具有不同的I/O复用方式,比如Solarisdev/poll文件,FressBSDkqueue机制,Linuxepoll系统调用
  • 对并发编程的支持,在多进程和多线程环境下,我们需要考虑各执行实体如何协同处理客户连接、信号和定时器,以避免竞态条件。

幸运的是,开源社区提供了很多优秀的I/O框架库,他们不仅解决了上述问题,让开发者可以将精力完全放在程序的逻辑上,而且稳定性、性能等各方面都相当出色。而Libevent就是其中相对轻量级的框架库。

I/O框架库概述

I/O框架库以库函数的形式,封装了较为底层的系统调用,给应用程序提供了一组更便于使用的接口。这些库函数往往比程序员自己实现的同样功能的函数更合理、更高效、且更健壮。因为它们经受住了真实网络环境下的高压测试,以及时间的考验。

各种I/O框架库的实现原理基本相似,要么以Reactor模式实现,要么以Procator模式实现(高性能服务器程序框架 - 两种高效的事件处理模式),要么同时以这两种模式实现。举例来说,基于Reactor模式的I/O框架库包含如下几个组件:

  • 句柄Handle
  • 事件多路分发器EventDemultiplexer
  • 事件处理器Eventhandler
  • 具体的事件处理器ConcreteEventHandler
  • Reactor

(推荐教程:Linux教程

这些组件关系如下图:

组件关系

  1. 句柄: I/O框架库要处理的对象,即I/O事件、信号和定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。在Linux环境下,I/O事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值。
  2. 事件多路分发器:事件的到来是随机的、异步的。我们无法预知程序何时收到一个客户连接请求,又亦活收到一个暂停信号。所以程序需要循环地等待并处理事件,这就是事件循环。在事件循环中,等待事件一般使用I/O复用技术来实现。I/O框架库一般将系统支持的各种I/O复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是selectpollepoll_wait等函数。此外事件多路分发器还需实现register_eventremove_event方法,以供调用者往事件多路分发器中添加事件和从事件多路分发器中删除事件。
  3. 事件处理器和具体时间处理器:事件处理器执行事件对应的业务逻辑。它通常包含一个或多个handle_event回调函数,这些回调函数在事件循环中被执行。I/O框架库提供的事件处理器通常是一个接口,用户需要继承它来实现自己的事件处理器,即具体事件处理器。因此,事件处理器中的回调函数一般被声明为需函数,以支持用户的扩展。此外,事件处理器一般还提供一个get_handle方法,它返回与该事件处理器关联的句柄。那么事件处理器和句柄有什么关系?当时间多路分发器检测到有事件发生时,它是通过句柄来通知应用程序的。因此,我们必须将事件处理器和句柄绑定,才能在事件发生时获取到正确的事件处理器。
  4. Reactor:Reactor是I/O框架的核心。它提供的几个主要方法是:
    • handle_events:该方法执行事件循环。它重复如下过程:等待事件,然后依次处理所有就绪事件对应的事件处理器。
    • register_handler: 该方法调用事件多路分发器的register_event方法来往事件多路分发器中注册一个事件。 -remove_handler:该方法调用事件多路分发器的remove_event方法来往删除事件多路分发器中注册一个事件。

I/O框架库的工作时序如下:

I/O工作时序

Libevent源码分析

Libevent是开源社区的一款高性能的I/O框架库,具有如下特点:

  • 跨平台支持
  • 统一事件源
  • 线程安全
  • 基于Reactor模式的实现

(推荐微课:Linux微课

一个实例

下面是用Libevent库实现的一个“Hello World”程序。

include <sys/signal.h>

#include <event2/event.h>


void signal_cb(int fd, short event, void *argc)
{
    struct event_base* base = (event_base*)argc;
    struct timeval delay = {2, 0};
    printf("Caught an interrupt signal; exiting cleanly in two seconds....\n");
    event_base_loopexit(base, &delay);
}


void timeout_cb(int fd, short event, void* argc)
{
    printf("timeout\n");
}


int main(int argc, char const *argv[])
{
    struct event_base* base = event_base_new();
    struct event* signal_event = evsignal_new(base, SIGINT, signal_cb, base);
    event_add(signal_event, NULL);


    timeval tv = {1, 0};
    struct event* timeout_event = evtimer_new(base, timeout_cb, NULL);
    event_add(timeout_event, &tv);


    event_base_dispatch(base);


    event_free(timeout_event);
    event_free(signal_event);
    event_base_free(base);


    return 0;
}

上述代码虽然简单,但却基本描述了Libevent库的主要逻辑:

  1. 调用event_base_new函数创建event_base对象。一个event_base相当于一个Reactor实例。
  2. 创建具体的事件处理器,并设置它们所从属的Reactor实例。evsignal_newevtimer_new分别用于创建信号事务处理器和定时事件处理器。它们是定义在如下:

定义

define evsignal_new(b, x, cb, arg) \

    event_new((b), (x), EV_SIGNAL|EV_PERSIST, (cb), (arg))
#define evtimer_new(b, cb, arg)     event_new((b), -1, 0, (cb), (arg))

可见,他们的统一入口是event_new函数,即用于创建通用事件处理器的函数,定义如下:

event_new(struct event_base base, evutil_socket_t fd, short events, void (cb)(evutil_socket_t, short, void ), void arg)其中,base参数指定行

其中:

  • base参数指定新创建的事件处理器从属的Reactor
  • fd参数指定与事件处理器关联的句柄。创建I/O事件处理器时,应该给fd参数传递文件描述符;创建信号事件处理器时,应该给fd参数传递信号值,比如之前实例代码中的SIGINT;创建定时事件处理器时则应该给fd参数传递-1
  • events参数指定事件类型,定义如下:

    #define EV_TIMEOUT  0x01   /*定时事件*/
    #define EV_READ     0x02         /*可读事件*/
    #define EV_WRITE    0x04        /*可写事件*/
    #define EV_SIGNAL   0x08       /*信号事件*/
    #define EV_PERSIST  0x10     /*永久事件*/
    /*边缘触发事件,需要I/O复用系统调用支持,比如epoll */
    #define EV_ET       0x20

上述代码中,EV_PERSIST的作用是:事件被触发后,自动重新对这个event调用event_add函数。

  • cb参数指定目标事件对应的回调函数,相当于事件处理器handle_event方法.
  • arg则是Reactor传递给回调函数的参数。

event_new函数成功时返回一个event类型的对象,也就是Libevent的事件处理器。Libevent用单词“event”来描述事件处理器,而不是事件,所以约定如下:

  • 事件指的是一个句柄上绑定的事件,比如文件描述符 0 上的可读事件
  • 事件处理器,也就是event结构提类型的对象,除了包含事件必须具备的两个要素(句柄和事件类型)外,还有很多其他成员,比如回调函数
  • 事件由事件多路分发器管理,事件处理器则由事件队列管理,事件队列包括多种,比如event_base中的注册事件队列。
  • 事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理器中的回调函数。

  1. 调用event_add函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应的事件添加到事件多路分发器中。even_add函数相当于Reactor中的register_handler方法。
  2. 调用event_base_dispatch函数来执行事件循环
  3. 事件循环结束后,使用*_free系列释放系统资源

(推荐课程:Linux就该这么学

源代码组织结构

  • github地址:https://github.com/libevent/libevent
  • 头文件目录include/event2。该目录是自Libevent主板本升级到2.0之后引入的,是提供给应用程序使用的,比如event.h头文件是核心函数,http.h头文件提供HTTP协议相关服务,rpc.h头文件提供远程过程调用支持。
  • 源码根目录下的头文件。这些头文件分为两类:
  • 一类是对include/event2目录下的部分头文件的包装
  • 另外一类是供Libevent内部使用的辅助性头文件,它们的文件名都具有*-internal.h的形式。
  • 通用数据目录compat/sys。该目录下仅有一个文件----queue.h。它封装了跨平台的基础数据结构,包括单向链表、双向链表、队列、尾队列和循环队列。
  • sample目录。提供一些示例代码
  • test目录。提供一次额测试代码
  • WIN32-Code。提供Windows平台上的一些专用代码。
  • event.c文件。该文件时间Libevent的整体框架,主要是eventevent_base两个结构体的相关操作。
  • debpoll.ckqueue.cevport.cselect.cwin32select.cpoll.cepoll.c文件。它们分别封装了如下I/O复用机制:/dev/pollkqueueevent portsPOSIX selectWindows selectpollepoll。这些文件的主要内容相似,都是针对结构体eventop所定义的接口函数的具体实现。
  • minheap-internal.h:该文件实现了一个事件堆,以提供对定时事件的支持。
  • signal.c:提供对信号的支持。其内容也是针对结构体eventop所定义的接口函数的具体实现
  • evmap.c文件:它维护句柄(文件描述符或信号)与时间处理器的映射关系
  • event_tagging.c:提供往缓冲区中添加标记数据,比如一个正数,以及从缓冲区中读取标记数据的函数
  • event_iocp文件:提供对Windows IOCP(Input/Output Completion Port,输入输出完成端口)的支持
  • buffer*.c文件:提供对网络I/O缓冲的控制,包括:输入输出数据过滤,传输速率限制,使用SSL(Secure Sockets Layer)协议对应用数据进行保护,以及零拷贝文件传输等。
  • evthread*.c文件:提供对多线程的支持
  • listener.c:封装了对监听socket的操作,包括监听连接和接受连接
  • logs.c文件。它是Libevent的日志文件系统
  • evutil.cevutil_rand.cstrlcpy.carc4random.c文件:提供了一些基本操作,比如生成随机数、获取socket地址信息、读取文件、设置socket属性等
  • evdns.chttp.cevrpc.c地址信息:分别提供了对DNS协议、HTTP协议和RPC(Remote Procddure Call,远程过程调用)协议的支持
  • epoll_sub.c文件,该文件未见使用

在整个源码中,event-internal.hinclude/event2/event_struct.hevent.cevmap.c等4个文件最为重要。它们定义了eventevent_base结构体,并实现了这两个结构体的相关操作。

以上就是关于Linux中高性能I/O框架库Libevent的相关介绍了,希望对大家有所帮助。

1 人点赞