这篇文章主要讲一下Libevent
库的内容,顺便对I/O
库整体做个介绍。
Linux服务器程序必须处理的三类事件:
- I/O事件
- 信号
- 定时事件
在处理这三类事件时我们通常需要考虑如下三个问题:
- 统一事件源。很明显,统一处理这三类事件既能使代码简单易懂,又能避免一些潜在的逻辑错误。
- 可移植性。不同的操作系统具有不同的
I/O
复用方式,比如Solaris
的dev/poll
文件,FressBSD
的kqueue
机制,Linux
的epoll
系统调用 - 对并发编程的支持,在多进程和多线程环境下,我们需要考虑各执行实体如何协同处理客户连接、信号和定时器,以避免竞态条件。
幸运的是,开源社区提供了很多优秀的I/O
框架库,他们不仅解决了上述问题,让开发者可以将精力完全放在程序的逻辑上,而且稳定性、性能等各方面都相当出色。而Libevent
就是其中相对轻量级的框架库。
I/O框架库概述
I/O
框架库以库函数的形式,封装了较为底层的系统调用,给应用程序提供了一组更便于使用的接口。这些库函数往往比程序员自己实现的同样功能的函数更合理、更高效、且更健壮。因为它们经受住了真实网络环境下的高压测试,以及时间的考验。
各种I/O
框架库的实现原理基本相似,要么以Reactor
模式实现,要么以Procator
模式实现(高性能服务器程序框架 - 两种高效的事件处理模式),要么同时以这两种模式实现。举例来说,基于Reactor
模式的I/O
框架库包含如下几个组件:
- 句柄
Handle
- 事件多路分发器
EventDemultiplexer
- 事件处理器
Eventhandler
- 具体的事件处理器
ConcreteEventHandler
Reactor
这些组件关系如下图:
句柄: I/O
框架库要处理的对象,即I/O
事件、信号和定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。在Linux
环境下,I/O
事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值。
事件多路分发器:事件的到来是随机的、异步的。我们无法预知程序何时收到一个客户连接请求,又亦活收到一个暂停信号。所以程序需要循环地等待并处理事件,这就是事件循环。在事件循环中,等待事件一般使用I/O
复用技术来实现。I/O
框架库一般将系统支持的各种I/O
复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的demultiplex
方法是等待事件的核心函数,其内部调用的是select
、poll
、epoll_wait
等函数。此外事件多路分发器还需实现register_event
和remove_event
方法,以供调用者往事件多路分发器中添加事件和从事件多路分发器中删除事件。
事件处理器和具体时间处理器:事件处理器执行事件对应的业务逻辑。它通常包含一个或多个handle_event
回调函数,这些回调函数在事件循环中被执行。I/O
框架库提供的事件处理器通常是一个接口,用户需要继承它来实现自己的事件处理器,即具体事件处理器。因此,事件处理器中的回调函数一般被声明为需函数,以支持用户的扩展。此外,事件处理器一般还提供一个get_handle
方法,它返回与该事件处理器关联的句柄。那么事件处理器和句柄有什么关系?当时间多路分发器检测到有事件发生时,它是通过句柄来通知应用程序的。因此,我们必须将事件处理器和句柄绑定,才能在事件发生时获取到正确的事件处理器。
Reactor:Reactor是I/O框架的核心。它提供的几个主要方法是:
handle_events
:该方法执行事件循环。它重复如下过程:等待事件,然后依次处理所有就绪事件对应的事件处理器。register_handler
: 该方法调用事件多路分发器的register_event
方法来往事件多路分发器中注册一个事件。remove_handler
:该方法调用事件多路分发器的remove_event
方法来往删除事件多路分发器中注册一个事件。
I/O框架库的工作时序如下:
Libevent源码分析
Libevent是开源社区的一款高性能的I/O框架库,具有如下特点:
- 跨平台支持
- 统一事件源
- 线程安全
- 基于Reactor模式的实现
一个实例
下面是用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
库的主要逻辑:
-
调用
event_base_new
函数创建event_base
对象。一个event_base
相当于一个Reactor
实例。 -
创建具体的事件处理器,并设置它们所从属的
Reactor
实例。evsignal_new
和evtimer_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
中的注册事件队列。 - 事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理器中的回调函数。
调用event_add
函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应的事件添加到事件多路分发器中。even_add
函数相当于Reactor
中的register_handler
方法。
调用event_base_dispatch
函数来执行事件循环
事件循环结束后,使用*_free
系列释放系统资源
源代码组织结构
- 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
的整体框架,主要是event
和event_base
两个结构体的相关操作。debpoll.c
、kqueue.c
、evport.c
、select.c
、win32select.c
、poll.c
和epoll.c
文件。它们分别封装了如下I/O复用机制:/dev/poll
、kqueue
、event ports
、POSIX select
、Windows select
、poll
和epoll
。这些文件的主要内容相似,都是针对结构体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.c
、evutil_rand.c
、strlcpy.c
和arc4random.c
文件:提供了一些基本操作,比如生成随机数、获取socket
地址信息、读取文件、设置socket
属性等evdns.c、http.c
和evrpc.c
地址信息:分别提供了对DNS
协议、HTTP
协议和RPC
(Remote Procddure Call,远程过程调用)协议的支持epoll_sub.c
文件,该文件未见使用
在整个源码中,event-internal.h
、include/event2/event_struct.h
、event.c
和evmap.c
等4个文件最为重要。它们定义了event
和event_base
结构体,并实现了这两个结构体的相关操作。
以上就是关于Linux
中高性能I/O
框架库Libevent
的相关介绍了,希望对大家有所帮助。
推荐:
Linux服务器开发/高级架构师 系统学习公开课地址:https://ke.qq.com/course/417774?flowToken=1031343
欢迎朋友们加入C/C++Linux服务器开发/高级架构师群:960994558
群内提供免费的C/C++Linux服务器开发/高级架构师学习资料资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)