Linux服务器进程在处理三类事件(IO、信号、定时)时需要考虑以下问题:
1.统一事件源。统一处理这三类事件既能使代码简单易懂,又能避免一些潜在的逻辑错误,可用IO复用系统调用来管理所有事件。
2.可移植性。不同的操作系统有不同的IO复用方式,如Solaris的/dev/poll文件、FreeBSD的kqueue机制、Linux的epoll系列系统调用。
3.对并发编程的支持。在多进程和多线程环境下,我们需要考虑各执行实体如何协同处理客户连接、信号、定时器,以避免竞态条件。
有很多优秀的开源IO框架库,它们不仅解决了以上问题,让开发者可以将精力完全放在程序的逻辑上,而且稳定性、性能等各方面都很出色,如ACE、ASIO、Libevent,本章介绍其中相对轻量级的Libevent框架库。
IO框架库以库函数,封装了较为底层的系统调用,给应用提供了一组更便于使用的接口,这些库函数往往比程序员自己实现的同样功能的函数更合理、更高效、更健壮,因为它们经受住了真是网络环境下的高压测试,以及时间的考验。
各种IO框架库的实现基本原理相似,要么以Reactor模式实现,要么以Proactor模式实现,要么同时以这两种模式实现。例如,基于Reactor模式的IO框架库包含以下组件:句柄(Handle)、事件多路分发器(EventDemultiplexer)、事件处理器(EventHandler)、具体的事件处理器(ConcreteEventHandler)、Reactor。这些组件的关系见下图:
1.句柄。IO框架库要处理的对象,即IO事件、信号、定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用进程这一事件。在Linux环境下,IO事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值。
2.事件的到来时随机的、异步的,我们无法预知进程何时收到一个客户连接请求,或收到一个暂停信号,所以进程需要循环地等待并处理事件,这就是事件循环,在事件循环中,等待时间一般使用IO复用技术来实现。IO框架库一般将系统支持的各种IO复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是select、poll、epoll_wait等函数。
此外,事件多路分发器还需实现register_event和remove_event方法,以供调用者往事件多路分发器中添加事件和从事件多路分发器中删除事件。
3.事件处理器和具体时间处理器。事件处理器执行事件对应的业务逻辑,它通常包含一个或多个handle_event回调函数,这些回调函数在事件循环中被执行。IO框架库提供的事件处理器通常是一个接口,用户需要继承它来实现自己的事件处理器,即具体时间处理器,因此,事件处理器中的回调函数一般被声明为虚函数,以支持用户的扩展。
此外,事件处理器一般还提供get_handle方法,它返回与该事件处理器关联的句柄。当事件多路分发器检测到有事件发生时,它是通过句柄来通知应用进程的,由于我们将句柄和事件处理器绑定,因此应用才能通过句柄找到正确的事件处理器。
4.Reactor。它是IO框架库的核心,它提供的几个主要方法是:
(1)handle_events。该方法执行事件循环,它重复以下过程:等待事件,然后依次处理所有就绪事件对应的事件处理器。
(2)register_handler。该方法调用事件多路分发器的register_event方法来往事件多路分发器中注册一个事件。
(3)remove_handler。该方法调用事件多路分发器的remove_event方法来删除事件多路分发器中的一个事件。
Libevent是开源的高性能IO框架库,使用Livevent的著名案例有:高性能的分布式内存对象缓存软件memcached,Google浏览器Chromiun的Linux版本。Libevent的特点:
1.跨平台支持。Libevent支持Linux、UNIX、Windows。
2.统一事件源。Libevent对IO事件、信号、定时事件提供统一的处理。
3.线程安全。Libevent使用libevent_pthreads库来提供线程安全支持。
4.基于Reactor模式实现。
Libevent的官网是http://libevent.org/
,其中提供Libevent源码的下载,以及Libevent框架库的第一手文档,且源码和文档的更新也较为频繁。作者写作此书时使用的Libevent版本是2.0.19。
使用Libevent的简单实例:
#include
#include
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);
}
以上代码虽然简单,但基本描述了Libevent库的主要逻辑:
1.调用event_init函数创建event_base对象。一个event_base对象相当于一个Reactor实例。
2.创建具体的事件处理器,并设置它们所从属的Reactor实例。evsignal_new和evtimer_new分别用于创建信号事件处理器和定时时间处理器,它们是定义在include/event2/event.h文件中的宏:
可见它们的统一入口是event_new函数,即用于创建通用事件处理器(图12-1中的EventHandler)的函数,其定义是:
base参数指定新创建的事件处理器从属的Reactor。fd参数指定与该事件处理器关联的句柄,创建IO事件处理器时,应该给fd参数传递文件描述符的值;创建信号事件处理器时,应该给fd参数传递信号值;创建定时时间处理器时,应该给fd参数传递-1。events参数指定事件类型,其可选值如下:
EV_PERSIST的作用是:事件被触发后,自动重新对这个event调用event_add函数。
cb参数指定目标事件对应的回调函数,相当于图12-1中事件处理器的handle_event回调函数。arg参数是Reactor传递给回调函数的参数。
event_new函数成功时返回一个event类型的对象,即Libevent的事件处理器。Libevent用event描述事件处理器,而不是事件,可能会使读者混乱,因此我们有如下约定:
(1)事件指的是一个句柄上绑定的事件,如文件描述符0上的可读事件。
(2)事件处理器,也就是event结构体对象,除了包含事件必须具备的两个要素(句柄和事件类型)外,还有其他成员,如回调函数。
(3)事件由事件多路分发器管理,事件处理器则由事件队列管理。事件队列包括多种,如event_base中的注册事件队列、活动事件队列、通用定时器队列,以及evmap中的IO事件队列、信号事件队列。
(4)事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理器中的回调函数。
3.调用event_add函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应的事件添加到事件多路分发器中。event_add函数相当于Reactor中的register_handler方法。
4.调用event_base_dispatch指定事件循环。
5.时间循环结束后,使用*_free系列函数释放系统资源。
Libevent源码中的目录和文件按功能可划分为如下部分:
1.头文件目录include/event2。该目录是自Libevent主版本升级到2.0后引入的,在1.4及更老的版本中无此目录。该目录中的头文件是Libevent提供给应用程序使用的,如event.h头文件提供核心函数,http.h头文件提供HTTP协议相关服务,rpc.h头文件提供远程过程调用支持。
2.源码根目录下的头文件。这些头文件分为两类:一类是对include/event2目录下的部分头文件的包装,另一类是供Libevent内部使用的辅助性头文件,它们的文件名具有*-internal.h的形式。
3.通用数据结构目录conpat/sys。该目录下仅有一个文件——queue.h,它封装了跨平台的基础数据结构,包括单向链表、双向链表、队列、尾队列、循环队列。
4.sample目录。它提供一些示例程序。
5.test目录。它提供一些测试代码。
6.WIN32-Code目录。它提供Windows平台上的一些专用代码。
7.event.c文件。该文件实现Libevent的整体框架,主要是event和event_base两个结构体的相关操作。
8.devpoll.c、kqueue.c、evport.c、select.c、win32select.c、poll.c、epoll.c文件。它们分别封装了如下IO复用机制:/dev/poll、kqueue、event ports、POSIX select、Windows select、poll、epoll。这些文件的主要内容相似,都是针对结构体eventop所定义的接口函数的具体实现。
9.minheap-internal.h文件。该文件实现了一个时间堆,以提供对定时事件的支持。
10.signal.c文件。它提供对信号的支持,其内容也是针对结构体eventop所定义的接口函数的具体实现。
11.evmap.c文件。它维护句柄(文件描述符或信号)与事件处理器的映射关系。
12.event_tagging.c文件。它提供往缓冲区中添加标记数据(如一个整数),以及从缓冲区中读取标记数据的函数。
13.event_iocp.c文件。它提供对Windows IOCP(Input/Output Completion Port,输出输出完成端口)的支持。
14.buffer*.c文件。它提供对网络IO缓冲的控制,包括:输入输出数据过滤、传输速率限制、使用SSL(Secure Sockets Layer)协议对应用数据进行保护、零拷贝文件传输等。
15.evthread*.c文件。它提供对多线程的支持。
16.listener.c文件。它封装了对监听socket的操作,包括监听连接和接受连接。
17.logs.c文件。它是Libevent的日志系统。
18.evutil.c、evutil_rand.c、strlcpy.c、arc4random.c文件。它们提供一些基本操作,如生成随机数、获取socket地址信息、读取文件、设置socket属性等。
19.evdns.c、http.c、evrpc.c文件。它们分别提供了对DNS协议、HTTP协议、RPC(Remote Procedure Call,远程过程调用)协议的支持。
20.epoll_sub.c文件。该文件未见使用。
整个源码中,event-internal.h、include/event2/event_struct.h、event.c、evmap.c等4个文件最为重要,它们定义了event和event_base结构体,并实现了这两个结构体的相关操作。
Libevent中的事件处理器是event结构类型,event结构体封装了句柄、事件类型、回调函数、其他必要的标志和数据,该结构体在include/event2/event_struct.h文件中定义:
struct event
{
TAILQ_ENTRY(event) ev_active_next;
TAILQ_ENTRY(event) ev_next;
union {
TAILQ_ENTRY(event) ev_next_with_common_timeout;
int min_heap_idx;
} ev_timeout_pos;
evutil_socket_t ev_fd;
struct event_base *ev_base;
union {
struct {
TAILQ_ENTRY(event) ev_io_next;
struct timeval ev_timeout;
} ev_io;
struct {
TAILQ_ENTRY(event) ev_signal_next;
short ev_ncalls;
short *ev_pncalls;
} ev_signal;
} _ev;
short ev_events;
short ev_res;
short ev_flags;
ev_uint8_t ev_pri;
ev_uint8_t ev_closure;
struct timeval ev_timeout;
void (*ev_callback)(evutil_socket_t, short, void *arg);
void *ev_arg;
};
下面介绍event结构体中成员:
1.ev_events。它代表事件类型,其取值可以是代码清单12-2所示的标志的按位或(互斥的事件类型除外,如读写事件和信号事件不能同时被设置)。
2.ev_next。所有已经注册的事件处理器(包括IO事件处理器和信号事件处理器)通过该成员串联成一个尾队列,我们称之为注册事件队列。宏TAILQ_ENTRY是尾队列中的节点类型,它定义在compat/sys/queue.h文件中:
3.ev_active_next。所有被激活的事件处理器通过该成员串联成一个尾队列,我们称之为活动事件队列。活动事件队列不止一个,不同优先级的事件处理器被激活后将被插入不同的活动事件队列中。在事件循环中,Reactor将按优先级从高到低遍历所有活动事件队列,并依次处理其中的事件处理器。
4.ev_timeout_pos。这是一个联合体,它仅用于定时事件处理器,为讨论方便,后面我们称定时时间处理器为定时器,老版本的Libevent中,定时器都是由时间堆来管理的,但开发者认为有时使用简单的链表来管理定时器效率更高,因此,新版本Libevent引入了通用定时器的概念,这些定时器不是存储在时间堆中,而是存储在尾队列中,我们称之为通用定时器队列,对于通用定时器而言,ev_timeout_pos联合体的ev_next_with_common_timeout成员指出了该定时器在通用定时器队列中的位置,对于其他定时器而言,ev_time_pos联合体的min_heap_idx成员指出了该定时器在时间堆中的位置。一个定时器是否是通用定时器取决于其超时值大小,具体判断原则可参考event.c文件中的is_common_timeout函数。
5._ev。这是一个联合体,所有具有相同文件描述符值的IO事件处理器通过ev.ev_io…ev_io_next成员串联成一个尾队列,我们称之为IO事件队列;所有具有相同信号值的信号事件处理器通过ev.ev_signal.ev_signal_next成员串联成一个尾队列,我们称之为信号事件队列,ev.ev_signal.ev_ncalls成员指定信号事件发生时,Reactor需要执行多少次该事件对应的事件处理器中的回调函数,ev.ev_signal.ev_pncalls指针成员要么是NULL,要么指向ev.ev_signal.ev_ncalls。
在程序中,我们可能针对同一个socket文件描述符上的可读(可写)事件创建多个事件处理器(它们拥有不同的回调函数),当该文件描述符上有可读(可写)事件发生时,所有这些事件处理器都应该被处理,所以Libevent使用IO事件队列将具有相同文件描述符值的事件处理器组织在一起,这样,当一个文件描述符上有事件发生时,事件多路分发器能很快把所有相关的事件处理器添加到活动事件队列中。信号事件队列的存在也是由于相同的原因。
6.ev_fd。对于IO事件处理器,它是文件描述符值;对于信号事件处理器,它是信号值。
7.ev_base。该事件处理器从属的event_base实例。
8.ev_res。它记录当前激活事件的类型。
9.ev_flags。它是一些事件标志,其可选值定义在include/event2/event_struct.h文件中:
10.ev_pri。它指定事件处理器的优先级,值越小优先级越高。
11.ev_closure。它指定event_base执行事件处理器的回调函数时的行为,其可选值定义在event-internal.h文件中:
12.ev_timeout。它仅对定时器有效,指定定时器的超时值。
13.ev_callback。它是事件处理器的回调函数,由event_base调用,回调函数被调用时,它的3个参数分别被传入事件处理器的以下3个成员:ev_fd、ev_res、ev_arg。
14.ev_arg。回调函数的参数。
创建一个event对象的函数是event_new(及其变体),它在event.c文件中实现,该函数很简单,主要给event对象分配内存并初始化它的部分成员,因此我们不讨论它。event对象创建好后,应用需要调用event_add函数将其添加到注册事件队列中,并将对应的事件注册到事件多路分发器上。event_add函数在event.c文件中实现,主要是调用另一个内部函数event_add_internal:
// 添加定时器事件时,tv参数一定不为NULL
static inline int event_add_internal(struct event *ev, const struct timeval *tv,
int tv_is_absolute) {
struct event_base *base = ev->ev_base;
int ret = 0;
int notify = 0;
// 检查事件循环是否被锁住,如果被锁住,那么当前线程就能安全地执行相关操作
// 如果事件循环未被锁住,那么这个断言将失败,可能导致程序崩溃
// 这只是一种调试工具,用于确保在需要时正确地锁定了event_base
EVENT_BASE_ASSERT_LOCKED(base);
// _event_debug_assert_is_setup用于断言ev是否已经被设置了
_event_debug_assert_is_setup(ev);
// event_debug是一个宏,为了避免运算优先级问题,它的参数使用了括号括起来,因此出现了双层括号
// 该宏的作用是便于调试,当定义了_EVENT_DEBUG时,它会打印出一条调试信息
event_debug((
"event_add: event: %p (fd %d), %s%s%scall %p",
ev,
(int)ev->ev_fd,
ev->ev_events & EV_READ ? "EV_READ " : " ",
ev->ev_events & EV_WRITE ? "EV_WRITE " : " ",
tv ? "EV_TIMEOUT " : " ",
ev->ev_callback));
// 此处的断言的作用为检查事件的标志是否都在EVLIST_ALL定义的范围内
EVUTIL_ASSERT(!(ev->ev_flags & ~EVLIST_ALL));
// 如果新添加的事件处理器尚未被添加到通用定时器队列或时间堆中,则为该定时器在时间堆上预留一个位置
if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {
if (min_heap_reserve(&base->timeheap, 1 + min_heap_size(&base->timeheap)) == -1) {
return -1;
}
}
// 如果没有定义此宏,说明没有禁用线程支持
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
// 如果主线程正在执行该信号处理器的回调函数,且要添加的事件处理器是信号事件处理器
// 且当前线程不是事件循环所在线程时,则当前调用者必须等待主线程完成调用,否则将引起竞态条件
// 因为我们要调用event结构体中的回调函数的ev_ncalls次,可能某次调用后就切换了回调函数
if (base->current_event == ev && (ev->ev_events & EV_SIGNAL) && !EVBASE_IN_THREAD(base)) {
++base->current_event_waiters;
// 使当前线程等待,直到事件处理完成
EVTHREAD_COND_WAIT(base->current_event_cond, base->th_base_lock);
}
#endif
// 如果是读、写、信号事件处理器,且该处理器不在注册事件队列和活动事件队列,则需要将其添加到事件循环
if ((ev->ev_events & (EV_READ | EV_WRITE | EV_SIGNAL))
&& !(ev->ev_flags & (EVLIST_INSERTED | EVLIST_ACTIVE))) {
// 如果是读、写事件处理器,添加IO事件和IO事件处理器的映射关系
if (ev->ev_events & (EV_READ | EV_WRITE))
res = evmap_io_add(base, ev->ev_fd, ev);
// 如果是信号事件处理器,则添加信号事件和信号事件处理器的映射关系
} else if (ev->ev_events & EV_SIGNAL) {
res = evmap_signal_add(base, (int)ev->ev_fd, ev);
}
if (res != -1) {
// 将事件处理器插入注册事件队列
event_queue_insert(base, ev, EVLIST_INSERTED);
}
if (res == 1) {
// 事件多路分发器中添加了新事件,通知主线程
notify = 1;
res = 0;
}
}
// 将事件处理器添加至通用定时器队列或时间堆中,对于信号事件处理器和IO事件处理器
// 根据evmap_*_add函数的结果决定是否添加(这是为了给事件设置超时)
// 而对于定时器,则始终应该添加它
if (res != -1 && tv != NULL) {
struct timeval now;
int common_timeout;
// 对于永久性事件处理器,如果其超时时间不是绝对时间,则将该事件的超时时间记录在ev->ev_io_timeout中
// ev_io_timeout是定义在event-internal.h文件中的宏:#define ev_io_timeout _ev.ev_io.ev_timeout
if (ev->ev_closure == EV_CLOSURE_PERSIST && !tv_is_absolute) {
ev->ev_io_timeout = *tv;
}
// 如果该事件处理器已经被插入通用定时器队列或时间堆中,则先删除它
if (ev->ev_flags & EVLIST_TIMEOUT) {
// 如果该事件处理器在时间堆顶部,这意味着这个事件是下一个即将超时的事件,此时通知主线程(事件循环)
if (min_heap_elt_is_top(ev)) {
notify = 1;
}
// 从超时事件列表中移除本事件
event_queue_remove(base, ev, EVLIST_TIMEOUT);
}
// 如果待添加的事件处理器已被激活,且激活原因是超时,则从活动事件队列中删除它,以避免其回调函数被执行
// 对于信号事件处理器,必要时还需将其ncalls成员设为0(ev_pncalls如果不为NULL,则它指向ev_ncalls)
// 信号事件被触发时,ev_ncalls指定其回调函数被执行的次数,将ev_ncalls置为0,可以干净地终止信号事件的处理
if ((ev->ev_flags & EVLIST_ACTIVE) && (ev->ev_res & EV_TIMEOUT)) {
if (ev->ev_events & EV_SIGNAL) {
if (ev->ev_ncalls && ev->ev_pncalls) {
*ev->ev_pncalls = 0;
}
}
// 从活动时间队列中删除事件处理器
event_queue_remove(base, ev, EVLIST_ACTIVE);
}
gettime(base, &now);
common_timeout = is_common_timeout(tv, base);
if (tv_is_absolute) {
ev->ev_timeout = *tv;
// 判断应该将定时器插入通用定时器还是时间堆
} else if (common_timeout) {
struct timeval tmp = *tv;
// 我们只关心毫秒中,MICROSECONDS_MASK中1对应的位
tmp.tv_usec &= MICROSECONDS_MASK;
// evutil_timeradd函数将前两个参数的时间相加,存到第三个参数中
evutil_timeradd(&now, &tmp, &ev->ev_timeout);
ev->ev_timeout.tv_usec |= (tv->tv_usec & ~MICROSECONDS_MASK);
} else {
// 加上当前系统时间,以取得定时器超时的绝对时间
evutil_timeradd(&now, tv, &ev->ev_timeout);
}
event_debug((
"event_add: timeout in %d seconds, call %p",
(int)tv->tv_sec, ev->ev_callback));
// 插入定时器
event_queue_insert(base, ev, EVLIST_TIMEOUT);
if (common_timeout) {
struct common_timeout_list *ctl = get_common_timeout_list(base, &ev->ev_timeout);
// 如果被插入的事件处理器是通用定时器队列中的第一个元素,则通过调用common_timeout_schedule
// 将其转移到时间堆中,这样,通用定时器链表和时间堆中的定时器就得到了统一的处理
if (ev == TAILQ_FIRST(&ctl->events)) {
common_timeout_schedule(ctl, &now, ev);
}
} else {
if (min_heap_elt_is_top(ev)) {
notify = 1;
}
}
}
// 如果有必要,唤醒主线程
if (res != -1 && notify && EVBASE_NEED_NOTIFY(base)) {
evthread_notify_base(base);
}
// _event_debug_note_add函数用于在调试模式下添加关于事件ev的调试信息记录
_event_debug_note_add(ev);
return res;
}
以上函数中调用了几个重要函数:
1.evmap_io_add。该函数将IO事件添加到事件多路分发器中,并将对应事件处理器添加到IO事件队列中,同时建立IO时间和IO事件处理器之间的映射关系。
2.evmap_signal_add。该函数将信号事件添加到事件多路分发器中,并将对应的事件处理器添加到信号事件队列中,同时建立信号事件和信号事件处理器之间的映射关系。
3.event_queue_insert。该函数将事件处理器添加到各种事件队列中:将IO事件处理器和信号事件处理器插入注册事件队列;将定时器插入通用定时器队列或时间堆;将被激活的事件处理器添加到活动事件队列中,其实现如下所示:
static void event_queue_insert(struct event_base *base, struct event *ev, int queue) {
EVENT_BASE_ASSERT_LOCKED(base);
// 避免重复插入
if (ev->ev_flags & queue) {
// Double insertion is possible for active events
if (queue & EVLIST_ACTIVE) {
return;
}
// event_errx函数生成一个错误消息并将其打印到标准错误
// __func__是一个预定义的标识符,用于在函数内部获取函数名
event_errx(1, "%s: %p(fd %d) already on queue %x", __func__, ev, ev->ev_fd, queue);
return;
}
// 如果不是内部事件,则增加event_base拥有的事件处理器总数
if (~ev->ev_flags & EVLIST_INTERNAL) {
++base->event_count;
}
// 标记此事件已被添加过
ev->ev_flags |= queue;
switch (queue) {
case EVLIST_INSERTED:
// 将IO事件处理器或信号事件处理器插入注册事件队列
TAILQ_INSERT_TAIL(&base->eventqueue, ev, ev_next);
break;
case EVLIST_ACTIVE:
++base->event_count_active;
// 将就绪事件处理器插入活动事件队列
TAILQ_INSERT_TAIL(&base->activequeues[ev->ev_pri], ev, ev_active_next);
break;
case EVLIST_TIMEOUT:
// 将定时器插入通用定时器队列或时间堆
if (is_common_timeout(&ev->ev_timeout, base)) {
struct common_timeout_list *ctl = get_common_timeout_list(base, &ev->ev_timeout);
insert_common_timeout_inorder(ctl, ev);
} else {
min_heap_push(&base->timeheap, ev);
}
break;
default:
event_errx(1, "%s: unknown queue %x", __func__, queue);
}
}
以上event_queue_insert函数所做的仅仅是将一个事件处理器加入event_base的某个事件队列中,对于新添加的IO事件处理器和信号事件处理器,我们还需让事件多路分发器来监听其对应的事件,同时建立文件描述符、信号值、事件处理器之间的映射关系,这需要通过evmap_io_add和evmap_signal_add函数来完成,这两个函数相当于事件多路分发器中的register_event方法,它们由evmap.c文件实现,但在讨论这两个函数前,我们先介绍一下它们将用到的一些重要数据结构:
// 如果定义了EVMAP_USE_HT,代表使用哈希表存储映射关系
#ifdef EVMAP_USE_HT
// ht-internal.h头文件提供对哈希表的支持
#include "ht-internal.h"
struct event_map_entry;
// HT_HEAD宏创建了类型为event_io_map的哈希表,其中的节点类型为event_map_entry
// 用该哈希表存储event_map_entry对象和IO事件队列(具有同样描述符值的IO事件处理器构成的IO事件队列)之间的映射关系
HT_HEAD(event_io_map, event_map_entry);
#else
// 如果没有定义EVMAP_USE_HT,则event_io_map与下面定义的event_signal_map一样
#define event_io_map event_signal_map
#endif
// 这段代码的目的是根据平台和配置选择适当的数据结构来存储事件映射
// 在Windows平台上,文件描述符可能是一个较大的值,不适合放入数组中
// 因此,在这种情况下,使用哈希表可以提供更有效的存储和查找
// 而在其他平台上(如Linux),文件描述符通常从0开始递增,因此可以直接使用数组来存储事件映射
struct event_signal_map {
// 用于存放evmap_io或evmap_signal的数组
void **entries;
int nentries;
};
// 如果定义了EVMAP_USE_HT,则哈希表event_io_map中的成员类型为event_map_entry
struct event_map_entry {
// map_node成员的类型由宏HT_ENTRY(event_map_entry)决定
HT_ENTRY(event_map_entry) map_node;
evutil_socket_t fd;
// 可能在未来的Libevent版本中,会添加其他成员到此union中,这样更省空间
union {
struct evmap_io evmap_io;
} ent;
};
// event_list是由event组成的尾队列,前面讨论的所有事件队列都是这种类型
TAILQ_HEAD(event_list, event);
// IO事件队列(确切地说,evmap_io.events才是IO事件队列)
struct evmap_io {
struct event_list events;
// 本IO事件队列关联的文件描述符上有多少读事件
ev_uint16_t nread;
// 本IO事件队列关联的文件描述符上有多少写事件
ev_uint16_t nwrite;
};
// 信号事件队列(确切地说,evmap_signal.events才是信号事件队列)
struct evmap_signal {
struct event_list events;
};
evmap_io_add和evmap_signal_add函数的逻辑基本相同,因此我们仅讨论evmap_io_add函数:
int evmap_io_add(struct event_base *base, evutil_socket_t fd, struct event *ev) {
// 获取event_base的后端IO复用机制实例
const struct eventop *evsel = base->evsel;
// 获取event_base中文件描述符与IO事件队列的映射表(哈希表或数组)
struct event_io_map *io = &base->io;
// fd参数对应的IO事件队列(同一文件描述符上可能有多个事件处理器,这些事件处理器在同一队列上)
struct evmap_io *ctx = NULL;
int nread, nwrite, retval = 0;
short res = 0, old = 0;
struct event *old_ev;
EVUTIL_ASSERT(fd == ev->ev_fd);
if (fd < 0) {
return 0;
}
// 如果不使用哈希表
#ifndef EVMAP_USE_HT
// IO事件队列io.entries中,每个文件描述符占用一项
// 如果fd大于当前数组的大小,则增加数组大小(扩大后的数组容量要大于fd)
if (fd >= io->nentries) {
if (evmap_make_space(io, fd, sizeof(struct evmap_io *)) == -1) {
return -1;
}
}
#endif
// 以下宏根据EVMAP_USE_HT是否定义而有不同的实现,但目的都是创建或获取fd的IO事件队列ctx
// 如果需要创建,则调用evmap_io_init进行初始化,并将evsel->fdinfo_len作为初始化函数的参数
// 然后在映射表io中为文件描述符fd和事件队列ctx添加映射关系
GET_IO_SLOT_AND_CTOR(ctx, io, fd, evmap_io, evmap_io_init, evsel->fdinfo_len);
nread = ctx->nread;
nwrite = ctx->nwrite;
if (nread) {
old |= EV_READ;
}
if (nwrite) {
old |= EV_WRITE;
}
if (ev->ev_events & EV_READ) {
if (++nread == 1) {
res |= EV_READ;
}
}
if (ev->ev_events & EV_WRITE) {
if (++nwrite == 1) {
res |= EV_WRITE;
}
}
// 检查读或写事件的是否超出最大限制,如果超过了,打印一条警告信息,然后返回-1
if (EVUTIL_UNLIKELY(nread > 0xffff || nwrite > 0xffff)) {
event_warnx("Too many events reading or writing on fd %d", (int)fd);
return -1;
}
// 如果运行在DEBUG模式,则检查是否在同一个文件描述符上混用了边沿触发和非边沿触发
if (EVENT_DEBUG_MODE_IS_ON() && (old_ev = TAILQ_FIRST(&ctx->events))
&& (old_ev->ev_events & EV_ET) != (ev->ev_events & EV_ET)) {
// 如果试图混用,则打印一条警告信息,然后返回-1
event_warnx("Tried to mix edge-triggered and non-edge-triggered events on fd %d",
(int)fd);
return -1;
}
if (res) {
void *extra = ((char *)ctx) + sizeof(struct evmap_io);
// 调用后端IO复用机制的add方法往事件多路分发器中添加新的事件
// add函数对不同的后端IO复用机制有不同的实现
if (evsel->add(base, ev->ev_fd, old, (ev->ev_events & EV_ET) | res, extra) == -1) {
return -1;
}
retval = 1;
}
ctx->nread = (ev_uint16_t)nread;
ctx->nwrite = (ev_unint16_t)nwrite;
// 将ev插到IO事件队列ctx的尾部
// ev_io_next是定义在event-internal.h文件中的宏:#define ev_io_next _ev.ev_io.ev_io_next
TAILQ_INSERT_TAIL(&ctx->events, ev, ev_io_next);
return retval;
}
eventop结构体封装了IO复用机制必要的一些操作,如注册事件、等待事件。它为event_base支持的所有后端IO复用机制提供了一个统一的接口,该结构体定义在event-internal.h文件中:
struct eventop {
// 后端IO复用技术的名称
const char *name;
// 初始化函数
void *(*init)(struct event_base *);
// 注册事件
int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
// 删除事件
int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
// 等待事件
int (*dispatch)(struct event_base *, struct timeval *);
// 释放IO复用机制使用的资源
void (*dealloc)(struct event_base *);
// 程序调用fork后是否需要重新初始化event_base
int need_reinit;
// IO复用技术支持的一些特性,是以下可选值的按位或:
// EV_FEATURE_ET:支持边沿触发事件EV_ET
// EV_FEATURE_O1:事件检测算法的复杂度是O(1)
// EV_FEATURE_FDS:不仅能监听socket上的事件,还能监听其他类型文件描述符上的事件
enum event_method_feature features;
// 有的IO复用机制需要为每个IO事件队列和信号事件队列分配额外的内存,该内存用于存放文件描述符
// 以避免同一个文件描述符被重复插入IO复用机制的事件表中
// evmap_io_add和evmap_io_del函数在调用eventop的add或del方法时,将这段内存的起始地址传递给该方法
// fdinfo_len指定了这段内存的长度
size_t fdinfo_len;
};
devpoll.c、kqueue.c、evport.c、select.c、win32select.c、poll.c、epoll.c文件分别针对不同的IO复用技术实现了eventop定义的这套接口,在支持多种IO复用技术的系统上,Libevent选择使用哪个取决于这些IO复用技术的优先级。Libevent支持的后端IO复用技术及它们的优先级定义在event.c文件中:
#ifdef _EVENT_HAVE_EVENT_PORTS
extern const struct eventop evportops;
#endif
#ifdef _EVENT_HAVE_SELECT
extern const struct eventop selectops;
#endif
#ifdef _EVENT_HAVE_POLL
extern const struct eventop pollops;
#endif
#ifdef _EVENT_HAVE_EPOLL
extern const struct eventop epollops;
#endif
#ifdef _EVENT_HAVE_WORKING_KQUEUE
extern const struct eventop kqops;
#endif
#ifdef _EVENT_HAVE_DEVPOLL
extern const struct eventop devpollops;
#endif
#ifdef WIN32
extern const struct eventop win32ops;
#endif
static const struct eventop *eventops[] = {
#ifdef _EVENT_HAVE_EVENT_PORTS
&evportops,
#endif
#ifdef _EVENT_HAVE_WORKING_KQUEUE
&kqops,
#endif
#ifdef _EVENT_HAVE_EPOLL
&epollops,
#endif
#ifdef _EVENT_HAVE_DEVPOLL
&devpollops,
#endif
#ifdef _EVENT_HAVE_POLL
&pollops,
#endif
#ifdef _EVENT_HAVE_SELECT
&selectops,
#endif
#ifdef WIN32
&win32ops,
#endif
NULL
};
Libevent通过遍历eventops数组来选择其后端IO复用技术,数组中首个元素优先级最高,最后一个元素最低,所以,在Linux下,Libevent默认选择的后端IO复用技术是epoll,但我们可以修改以上代码来选择不同的后端IO复用技术。
结构体event_base是Libevent的Reactor,它定义在event-internal.h文件中:
struct event_base {
// 初始化Reactor时选择一种后端IO复用机制,并记录在该字段中
const struct eventop *evsel;
// 指向IO复用机制真正存储的数据,它通过evsel成员的init函数来初始化
void *evbase;
// 事件变化队列,用途是:如果一个文件描述符上注册的事件被多次修改,则可使用缓冲来避免重复的系统调用(如epoll_ctl函数)
// 它仅能用于时间复杂度为O(1)的IO复用技术
struct event_changelist changelist;
// 指向信号的后端处理机制,目前仅在signal.h文件中定义了一种处理方法
const struct eventop *evsigsel;
// 信号事件处理器使用的数据结构,其中封装了一个由socketpair函数创建的管道
// 它用于信号处理函数和事件多路分发器之间的通信,与统一事件源的思路相同
struct evsig_info sig;
// 添加到该event_base的虚拟事件、所有事件、激活事件的数量
int virtual_event_count;
int event_count;
int event_count_active;
// 是否执行完活动事件队列上剩余的任务后就退出事件循环
int event_gotterm;
// 是否立即退出事件循环,而不管是否还有任务需要处理
int event_break;
// 是否应启动一个新的事件循环
int event_continue;
// 目前正在处理的活动事件队列的优先级
int event_running_priority;
// 事件循环是否已启动
int running_loop;
// 活动事件队列数组,索引值越小的队列,优先级越高,高优先级的活动事件队列中的事件处理器将被优先处理
struct event_list *activequeues;
// 活动事件队列数组的大小,即该event_base一共有nactivequeues个不同优先级的活动事件队列
int nactivequeues;
// 以下3个成员管理通用定时器队列
struct common_timeout_list **common_timeout_queues;
int n_common_timeouts;
int n_common_timeouts_allocated;
// 存放延迟回调函数的链表,事件循环每次成功处理完一个活动事件队列中的所有事件后,就调用一次延迟回调函数
struct deferred_cb_queue defer_queue;
// 文件描述符和IO事件之间的映射关系表
struct event_io_map io;
// 信号值和信号事件之间的映射关系表
struct event_signal_map sigmap;
// 注册事件队列,存放IO事件处理器和信号事件处理器
struct event_list eventqueue;
// 时间堆
struct min_heap timeheap;
// 管理系统时间的一些成员
struct timeval event_tv;
struct timeval tv_cache;
#if defined(_EVENT_HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
struct timeval tv_clock_diff;
time_t last_updated_clock_diff;
#endif
// 多线程支持
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
// 当前运行该event_base的事件循环的线程
unsigned long th_owner_id;
// 对event_base的独占锁
void *th_base_lock;
// 当前事件循环正在执行哪个事件处理器的回调函数
struct event *current_event;
// 条件变量,用于唤醒正在等待某个事件处理完毕的线程
void *current_event_cond;
// 等待current_event_cond的线程数
int current_event_waiters;
#endif
#ifdef WIN32
struct event_iocp_port *iocp;
#endif
// 该event_base的一些配置参数
enum event_base_config_flag flags;
// 以下成员给工作线程唤醒主线程提供了方法(使用socketpair函数创建的管道)
int is_notify_pending;
evutil_socket_t th_notify_fd[2];
struct event th_notify;
int (*th_notify_fn)(struct event_base *base);
};
最后讨论一下Libevent的动力,即事件循环。Libevent中实现事件循环的函数是event_base_loop,该函数首先调用IO事件多路分发器的事件监听函数,以等待事件,当有事件发生时,就依次处理之:
int event_base_loop(struct event_base *base, int flags) {
const struct eventop *evsel = base->evsel;
struct timeval tv;
struct timeval *tv_p;
int res, done, retval = 0;
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
// 一个event_base仅允许运行一个事件循环
if (base->running_loop) {
event_warnx("%s: reentrant invocation. Only one event_base_loop can run on each"
"event_base at once.", __func__);
EVBASE_RELEASE_LOCK(base, th_base_lock);
return -1;
}
base->running_loop = 1; // 标记event_base已经开始运行
clear_time_cache(base); // 清除event_base的系统时间缓存
// 设置信号事件的event_base实例
if (base->sig.ev_signal_added && base->sig.ev_n_signals_added) {
event_set_base(base);
}
done = 0;
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
base->th_owner_id = EVTHREAD_GET_ID();
#endif
base->event_gotterm = base->event_break = 0;
while (!done) {
base->event_continue = 0;
if (base->event_gotterm) {
break;
}
if (base->event_break) {
break;
}
// 校准tv的时间
timeout_correct(base, &tv);
tv_p = &tv;
if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
// 获取时间堆顶上元素的超时值,即IO复用系统调用本次应设置的超时值
timeout_next(base, &tv_p);
} else {
// 此时如果有就绪事件未处理,则将IO复用系统调用的超时时间置0
// 这样IO复用系统调用将直接返回,进程就能直接处理就绪事件了
// 此时如果没有就绪事件未处理,说明执行非阻塞事件循环,即,将超时时间置0
evutil_timerclear(&tv);
}
// 如果event_base中没有注册任何事件,且没有就绪事件,则直接退出事件循环
if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
event_debug(("%s: no events registered.", __func__));
retval = 1;
goto done;
}
// 更新时间,并清空时间缓存
gettime(base, &base->event_tv);
clear_time_cache(base);
// 调用事件多路分发器的dispatch方法等待事件,将就绪事件插入活动事件队列
res = evsel->dispatch(base, tv_p);
if (res == -1) {
event_debug(("%s: dispatch returned unsuccessfully.", __func__));
retval = -1;
goto done;
}
// 将时间缓存更新为当前系统时间
update_time_cache(base);
// 检查时间堆上的到期事件并依次执行之
timeout_process(base);
if (N_ACTIVE_CALLBACKS(base)) {
// 调用event_process_active依次处理就绪的信号事件和IO事件
int n = event_proess_active(base);
if ((flags & EVLOOP_ONCE) && N_ACTIVE_CALLBACKS(base) == 0 && n != 0) {
done = 1;
}
// 如果是非阻塞调用事件循环,则直接返回
} else if (flags & EVLOOP_NONBLOCK) {
done = 1;
}
}
event_debug(("%s: asked to terminate loop.", __func__));
done:
// 事件循环结束,清空时间缓存,并设置停止循环标志
clear_time_cache(base);
base->running_loop = 0;
EVBASE_RELEASE_LOCK(base, th_base_lock);
return retval;
}