高性能I/O框架---Libevent

一、Linux服务端程序处理事件的3个关键点

  1. 统一事件源:
    信号是一种异步事件,信号处理函数和程序的主循环是两种不同的执行线路。信号处理函数通常使用管道来将信号传递给主循环,信号处理函数往管道的写端写入信号量,主函数则从管道的独断读出信号量,为了监听管道上何时有数据可读,使用I/O复用系统调用监听管道读端文件描述符上的可读事件。因此信号事件就能和其他I/O事件一样被处理,即统一事件源。
  2. 可移植性
    不同的操作系统有不同的I/O复用方式,Linux的epoll系统调用,Solaris的dev/poll文件等。
  3. 对并发编程的支持
    多进程多线程如何协同工作。

二、I/O框架库

I/O框架库以库函数的形式,封装了较为底层的系统调用,给应用程序提供了一组更便于使用的接口,这些库函数往往比程序员自己实现的同样的函数更合理更有效,且更健壮。I/O框架的实现原理模式为reactor,proactor,或者同时两种模式。

(一)基于reactor模式的I/O框架库组件

我们主要了解一下reactor模式:基于reactor模式的I/O框架库包含如下几个组件:句柄,事件多路分发器,事件处理器和具体的事件处理器,reactor。

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

三、Libevent

我们今天主要学习高性能I/O框架Libevent,它将底层的I/O复用进行封装。

(一)特点:

  • 跨平台支持:支持Linux,unix,windows等。
  • 统一事件源:可以对I/O事件,信号和定时事件提供统一的处理。
  • 线程安全:libevent使用libevent_pthreads库来提供线程安全支持。
  • 基于reactor模式的实现,即主线程只负责监听,工作线程负责读写,逻辑处理。

使用libevent的著名案例有:高性能的分布式内存对象缓存软件memcached,Google浏览器Chromiun的Linux版本等。

(二)使用原理

我们先不要去看libevent内部是什么,先理解一下它在一个程序种承担什么角色。
在这里插入图片描述
我们可以看到原来我们需要自己写的I/O复用函数现在Libevent帮我们实现了,所以你可以理解它的作用就和select,poll,epoll是一个作用,但比他们出色很多。

(三)基于reactor模式的I/O框架库Libevent组件

1. 句柄:
I/O事件句柄为为文件描述符fd,信号事件句柄为信号量,定时事件句柄为时间。
2. 事件多路并发器:
I/O复用实现
3.事件处理器和具体事件处理器:
总共有3种事件类型,信号,定时,I/O,它们的统一创建入口为event_new函数:

struct event* event_new(struct event_base* base,evutil_socket_t fd,short events,void(cb)(evutil_socket_t,short,void),void* arg)返回一个事件处理器

  • 其中base参数指定新创建的事件处理器从属的Reactor;
  • fd参数指定与该事件处理器关联的句柄,创建I/O事件处理器时,应该给fd参数传递文件描述符,信号事件处理器传递信号值,创建定时事件处理器传递-1或者时间设置结构体;
  • events参数指定事件类型,libevent支持的事件类型:
事件类型含义
#define EV_TIMEOUT定时事件
#define EV_READ可读事件
#define EV_WRITE可写事件
#define EV_SIGNAL信号事件
#define EV_PERSIST永久事件
#define EV_ET边沿触发事件,即只调用一次epoll_wait()
  • cb参数指定目标事件对应的回调函数,即需要实现的逻辑业务处理
  • arg为Reactor传递给回调函数的参数。

我们创建3个处理器:
(1)创建信号事件处理器:

#define evsignal_new(b,x,,cb,arg)\
event_new((b),(x),EV_SIGNAL|EV_PERSISR,(cb),(arg)信号事件处理器关注信号事件和永久事件,所以可以触发多次。

struct event *signal_event=evsignal_new(base,SIGINT,signal_cb,NULL);创建一个信号事件具体处理器结构体,并设置它们所从属Reactor实例,创建事件的参数为base(reactor实例),SIGINT(事件类型为信号事件)signal_cb(回调函数),NULL(回调函数参数)
(2) 创建定时事件处理器:

#define evtimer_new(b,cb,arg) event_new((b),-1,0,(cb),arg)
struct timeval cv={2,0}//超时时间结构体

struct event *timeout_event=evtimer_new(base,timeout,timeout_cb,NULL)
(3) 创建I/O事件处理器
struct event *listen_ev = event_new(base,sockfd,EV_READ|EV_PERSIST,listen_cb,base);

4. Reactor
(1)创建Reactor struct event_base *base=event_init(),一个event_base就是一个reactor实例,
(2) event_add()进行事件的注册,每定义一个事件处理器都要进行添加

  • 把信号事件处理器进行添加:event_add(signal_event,NULL);
  • 把定时事件处理器进行添加:event_add(timeout_event,&tv)
  • 把I/O事件处理器进行添加:event_add(listen_ev,NULL);

(3)event_base_dispatch(base) 事件循环执行,这样它就检测这3个事件处理器上是否有就绪事件发生,有就调对应的回调函数。比如到了定时时间,它就检测到了事件就绪,调用timeout_cb进行处理。
(4)event_del()进行事件的删除。

(四)实现Libevent框架步骤

  1. event_init()生成reactor实例。
  2. event_new()生成并定义具体事件处理器,绑定reactor实例,确定回调函数cb和参数。
  3. 不要直接在主函数写,封装写外面。实现回调函数voidcb(int fd,short event,void *arg)//文件描述符,事件类型,刚刚传进来的参数文件描述符和事件类型内核会处理,我们不需要动,每个回调函数接口都是一样的,除了名字和实现不一样其他都一样。
  4. event_add()添加事件处理器。
  5. event_base_dispatch()启动事件循环,就是内核调用epoll_wait(),监听这两个事件是否有事件发生,就是以前我们需要实现的监听事件,调用函数内核都做了,我们只需要写好调用函数即可。这是个死循环,可以再定义一个退出事件,2秒后退出:
    在这里插入图片描述
  6. event_free()释放占用的资源:event_free(signal_event),event_free(timeout_event) Event_base_free(base)。

所以libevent的实现流程为:
在这里插入图片描述

(五)Libevent的一个实例

  1. 功能:检测定时和信号事件,超过2秒触发定时事件进行打印,每向终端发送信号触发信号事件进行打印。
  2. 编译:因为libevent是一个库,所以在编译时需要手动链接库:gcc -o main main.c -levent
  3. 代码:
# include<stdio.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<stdlib.h>
# include<signal.h>
# include<event.h>

//3.实现回调函数,只做打印
void signal_cb(int fd,short event,void *arg)
{
    printf("%d signal triggered\n",fd);
}
void timeout_cb(int fd,short event,void *arg)
{
    printf("%d timeout triggered\n",fd);
}
int main()
{
    //1.创建reactor实例
    struct event_base *base=event_init();
    //2.创建具体事件处理器
    //(1)信号事件处理器
    struct event *signal_event=evsignal_new(base,SIGINT,signal_cb,NULL);
    //(2)定时事件处理器
    struct event *timeout_event=evtimer_new(base,timeout_cb,NULL);
    //4.添加事件处理器
    event_add(signal_event,NULL);
    struct timeval tv={2,0};
    event_add(timeout_event,&tv);
    //5.启动事件循环
    event_base_dispatch(base);
    //释放占用资源
    event_free(signal_event);
    event_free(timeout_event);
    event_base_free(base);
}

  1. 结果:

在这里插入图片描述
开始执行事件循环,2s后就返回time out,因为定时事件关注永久性事件,所以只能触发一次。点击CRTL C发出信号,就会打印信号被触发,信号事件默认永久性事件,所以可以执行多次。
如果没写事件循环结束事件,我们可以利用kill杀死进程。

加油,加油哦!🤡

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值