文章目录
一、Linux服务端程序处理事件的3个关键点
- 统一事件源:
信号是一种异步事件,信号处理函数和程序的主循环是两种不同的执行线路。信号处理函数通常使用管道来将信号传递给主循环,信号处理函数往管道的写端写入信号量,主函数则从管道的独断读出信号量,为了监听管道上何时有数据可读,使用I/O复用系统调用监听管道读端文件描述符上的可读事件。因此信号事件就能和其他I/O事件一样被处理,即统一事件源。 - 可移植性:
不同的操作系统有不同的I/O复用方式,Linux的epoll系统调用,Solaris的dev/poll文件等。 - 对并发编程的支持:
多进程多线程如何协同工作。
二、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框架步骤
- event_init()生成reactor实例。
- event_new()生成并定义具体事件处理器,绑定reactor实例,确定回调函数cb和参数。
- 不要直接在主函数写,封装写外面。实现回调函数voidcb(int fd,short event,void *arg)//文件描述符,事件类型,刚刚传进来的参数文件描述符和事件类型内核会处理,我们不需要动,每个回调函数接口都是一样的,除了名字和实现不一样其他都一样。
- event_add()添加事件处理器。
- event_base_dispatch()启动事件循环,就是内核调用epoll_wait(),监听这两个事件是否有事件发生,就是以前我们需要实现的监听事件,调用函数内核都做了,我们只需要写好调用函数即可。这是个死循环,可以再定义一个退出事件,2秒后退出:
- event_free()释放占用的资源:event_free(signal_event),event_free(timeout_event) Event_base_free(base)。
所以libevent的实现流程为:
(五)Libevent的一个实例
- 功能:检测定时和信号事件,超过2秒触发定时事件进行打印,每向终端发送信号触发信号事件进行打印。
- 编译:因为libevent是一个库,所以在编译时需要手动链接库:gcc -o main main.c -levent
- 代码:
# 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);
}
- 结果:
开始执行事件循环,2s后就返回time out,因为定时事件关注永久性事件,所以只能触发一次。点击CRTL C发出信号,就会打印信号被触发,信号事件默认永久性事件,所以可以执行多次。
如果没写事件循环结束事件,我们可以利用kill杀死进程。
加油,加油哦!🤡