概述
在处理三类事件:I/O事件 信号 定时事件 的时候 需要考虑三个问题
- 统一事件源 统一处理三类事件 利用I/O复用系统调用来管理所有事件
- 可移植性 不同的操作系统具有不同的I/O复用方式,比如Solaris的dev/poll文件, FreeBSD的kqueue机制,Liunx的epoll系列系统调用
- 支持并发编程的支持,在多进程和多线程环境下,需要考虑个执行实体如何协同处理客户连接,信号,定时器,以避免竞态条件
开源社区中的ACE ASIO Libevent等I/O框架可以解决这些问题 让开发者可以将精力完全放在程序的逻辑。
下面介绍较为轻量级的Libevent框架
I/O框架库概述
I/O框架库以库函数的形式 封装了较为底层的系统调用,给程序提供了更便于使用的接口 ,这些库函数往往比程序员自己实现的同样功能函数更合理更高效,更健壮,且通过了真实的网络环境的高压测试,以及时间的考验。
I/O框架库实现的原理基本相识,要么是Reactor方式 要么以Proactor模式 要么两种方式同时来实现
比如基于Reactor模式的I/O框架库包含下面几个组件:
- 句柄(Handle)
- 事件多路分发器(EventDemultiplexer)
- 事件处理器(EventHandler)
- 具体事件处理器(ConcreteEventHandler)
- Reactor
1:句柄
I/O框架库要处理的对象 I/O事件 信号 定时事件 统一称为事件源 而事件源通常和一个句柄绑定在一起。句柄的作用 当内核检测到就绪事件的时候 就会通过句柄来通知应用程序这一事件。
在liunx环境下 I/O事件对应的句柄是文件描述符,信号事件对应的句柄就是信号量
2:事件多路分发器
事件的到来是追随的,异步的,无法预支程序何时收到一个客户连接请求。程序往往需要循环的等待并处理事件,这就是事件循环,在循环中,等待事件一般用I/O复用技术来实现。I/O框架库一般用系统支持的各种I/O复用接口 ,将其封装成统一的接口,统称为事件多路分发器。事件多路分发器的demultiplex函数就是等待事件的核心函数,其内部调用的是select poll epoll_wait函数
事件多路分发器 还需要实现register_event 和remove_event方法 也就是订阅者和分发者模式 我们内部需要处理什么样的事件 还需要通过业务决定 通过register_event 去订阅对应的事件
3: 事件处理器和具体事件处理器
事件处理器执行事件对应的业务,它通常包含一个或者多个handle_event回调函数 这些回调函数在事件循环中被执行。I/O框架库提供的事件处理器通常是一个接口 ,用户需要去重写它去,就是具体的事件处理器。事件处理器中的回调函数通常被声明为虚函数 ,支持用户扩展。
事件处理器一般还提供一个get_handle方法 。 它返回一个与事件处理器关联的句柄。当事件多路分发器检测到有事件发生的。它是通过句柄来通知应用程序,我们需要将事件处理器和句柄绑定在一起,才能在事件发生的时候 获取到正确的事件处理器。
4:Reactor
Reactor是I/O框架库的核心 。 它提供几个主要的方法是:
- handle_events 执行事件循环 等待事件 然后依次处理所有准备就绪的事件处理器。
- register_handle 通过调用此函数 往事件多路分发器中注册一个事件
- remove_handler 通过调用此函数 来删除多路事件分发器中注册的事件。
Libevent源码分析
Libevent是一款高性能的I/O框架图。使用Libevent的著名案例:高性能的分布式对象缓存软件memcached,Google浏览器Chromium的Liunx版本。
Libevent特点:
- 跨平台 支持Liunx UNIX 和Window
- 统一事件源 对于I/O事件 信号 定时器来统一处理
- 线程安全 Libevent使用libevent_pthreads库 来提供线程安全支持
- 基于Reactor模式的视线
一个实例
#include <sys/signal.h>
#include <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()
{
struct event_base* base = event_init();
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 );
}
上面代码主要逻辑:
1:调用event_init 来创建一个event_base 对象
2:创建具体的事件处理器 来设置它从属的Reactor实例。evsignal_new 和evtimer_new分别用于创建信号事件处理器和定时器处理器 他们是定义在event.h文件的宏
可见他们的统一入口是event_new函数 用于创建通用事件处理器 定义为
base参数指定新创建的事件处理器从属的reactor fd参数指定与该时间处理器相关联的句柄,创建I/O事件处理器。fd参数传递文件描述符。创建信号事件处理器时 给fd参数传递信号值,比如SIGINT。创建定时事件处理器的时候 应该给fd传递-1.events参数指定事件类型,其可选值定义在event.h
cb参数指定目标事件的回调函数
arg参数是Reactor传递给回调函数的参数
event_new函数成功时 返回一个event类型的对象 也是Libevent的事件处理器。
- 事件指的是一个句柄绑定的事件 比如文件描述符0上面的可读事件
- 事件处理器 也就是event结构体类型,除了包含事件必须具备的两个要素(句柄 和 事件类型) 还有很多其他类型 的成员
- 事件由事件多路分发器管理,事件处理器则由事件队列的管理。事件队列有多种 比如event_base中的注册事件队列,活动事件队列和通用定时器队列,以及evmap中的I/O事件队列 信号事件队列
- 事件循环对一个被激活的事件的处理方式就是调用对应的事件处理器中的回调函数
3:调用event_add函数,将事件处理器添加注册事件队列中,并将该时间处理器对应的事件添加到事件多路分发器中。event_add函数 相当于Reactor中register_handler方法
4:调用event_base_dispathch函数来执行事件循环
5: 事件循环接收后 使用*_free 系列函数来释放系统函数