Linux高性能服务器编程
本文是读书笔记,如有侵权,请联系删除。
参考
Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes
豆瓣: Linux高性能服务器编程
文章目录
- Linux高性能服务器编程
- 第12章 高性能I/O框架库Libevent
- 12.1 I/O框架库概述
- 12.2 Libevent 源码分析
- 12.2.1 一个实例
- 12.2.2 源代码组织结构
- 12.2.3 event 结构体
- 12.2.4 往注册事件队列中添加事件处理器
- 12.2.5往事件多路分发器中注册事件
- 12.2.6 eventop 结构体
- 12.2.7 event_base结构体
- 12.2.8 事件循环
- 后记
第12章 高性能I/O框架库Libevent
前面我们利用三章的篇幅较为细致地讨论了Linux服务器程序必须处理的三类事件:I/O 事件、信号和定时事件。在处理这三类事件时我们通常需要考虑如下三个问题:
统一事件源。很明显,统一处理这三类事件既能使代码简单易懂,又能避免一些潜在 的逻辑错误。前面我们已经讨论了实现统一事件源的一般方法——利用I/O复用系统 调用来管理所有事件。
可移植性。不同的操作系统具有不同的I/O复用方式,比如Solaris的dev/poll文件, FreeBSD的kqueue机制,Linux的epoll系列系统调用。
对并发编程的支持。在多进程和多线程环境下,我们需要考虑各执行实体如何协同处 理客户连接、信号和定时器,以避免竞态条件。
所幸的是,开源社区提供了诸多优秀的I/O框架库。它们不仅解决了上述问题,让开发者可以将精力完全放在程序的逻辑上,而且稳定性、性能等各方面都相当出色。比如ACE、 ASIO和Libevent。本章将介绍其中相对轻量级的Libevent框架库。
Libevent 是一个用于处理事件驱动编程的开源软件库。它提供了一个跨平台的、轻量级的事件通知库,用于开发高性能、可伸缩和并发的网络应用程序。下面是对 Libevent 的详细介绍:
主要特点和功能:
-
事件驱动: Libevent 基于事件驱动的编程模型。它能够监视各种事件,如网络套接字的可读、可写状态变化,定时器的到期等。一旦事件发生,Libevent 将执行注册的回调函数。
-
跨平台支持: Libevent 提供了对多个操作系统的支持,包括类 Unix 系统(Linux、BSD)、Windows 等。这使得开发者能够在不同平台上构建相同的事件驱动程序。
-
高性能: Libevent 被设计为高性能的事件通知库,通过使用底层的系统调用,如
select
、poll
、epoll
、kqueue
等,以达到最佳的性能表现。 -
支持多种网络编程模型: Libevent 提供了多种网络编程模型,包括基于回调函数的单线程模型和多线程/多进程模型,以满足不同应用场景的需求。
-
定时器支持: Libevent 允许创建定时器,以便在指定的时间后执行相应的操作。这对于实现超时、心跳等功能非常有用。
-
Buffer 操作: Libevent 提供了对缓冲区的支持,可以方便地进行数据读取和写入,同时处理边界问题。
-
SSL 支持: Libevent 支持通过 OpenSSL 或其他 SSL/TLS 库进行安全传输,以满足对安全性要求较高的应用程序。
-
轻量级: Libevent 的设计目标之一是轻量级,不引入过多的依赖,使得它成为一个理想的事件通知库选择。
主要组件:
-
Event Base: Libevent 事件库的核心结构,管理事件循环。
-
Event: 表示一个事件,可以是套接字事件、定时器事件等。
-
Buffer: 用于在事件处理中进行数据读写的缓冲区。
-
Evbuffer: 类似于 Buffer,是一个高级的缓冲区结构,提供了更多的操作。
使用场景:
-
网络服务器: Libevent 可以用于开发高性能的网络服务器,能够处理大量并发连接。
-
代理服务器: 由于 Libevent 支持多种网络编程模型,它适用于构建代理服务器,如反向代理、负载均衡器等。
-
实时系统: Libevent 可以用于实时系统,通过事件驱动模型实现高效的事件处理。
-
定时任务: 定时器功能使得 Libevent 可以被用于实现各种定时任务,如定时数据备份、定时任务调度等。
使用示例:
以下是一个简单的 Libevent 使用示例,实现了一个基本的 HTTP 服务器:
#include <event2/event.h>
#include <event2/http.h>
void request_handler(struct evhttp_request *req, void *arg) {
// 处理 HTTP 请求的回调函数
struct evbuffer *buf = evbuffer_new();
evbuffer_add_printf(buf, "Hello, Libevent!\n");
evhttp_send_reply(req, HTTP_OK, "OK", buf);
evbuffer_free(buf);
}
int main() {
struct event_base *base = event_base_new();
struct evhttp *http = evhttp_new(base);
evhttp_bind_socket(http, "0.0.0.0", 8080);
// 设置 HTTP 请求处理回调函数
evhttp_set_gencb(http, request_handler, NULL);
event_base_dispatch(base);
evhttp_free(http);
event_base_free(base);
return 0;
}
总结:
Libevent 是一个强大而灵活的事件驱动库,适用于开发高性能、并发的网络应用程序。它的跨平台支持、高性能、定时器功能等特点使得它在构建实时系统和网络服务器时成为一种有力的选择。
12.1 I/O框架库概述
I/O框架库以库函数的形式,封装了较为底层的系统调用,给应用程序提供了一组更便于使用的接口。这些库函数往往比程序员自己实现的同样功能的函数更合理、更高效,且更 健壮。因为它们经受住了真实网络环境下的高压测试,以及时间的考验。
各种I/O框架库的实现原理基本相似,要么以Reactor模式实现,要么以Proactor模式实现,要么同时以这两种模式实现。举例来说,基于Reactor模式的I/O框架库包含如下几个组件:句柄(Handle)、事件多路分发器(EventDemultiplexer)、事件处理器 (EventHandler)和具体的事件处理器(ConcreteEventHandler)、Reactor。这些组件的关系如 图12-1所示。
1.句柄
I/O框架库要处理的对象,即I/O事件、信号和定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。在Linux环境下,I/O事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值。
2.事件多路分发器
事件的到来是随机的、异步的。我们无法预知程序何时收到一个客户连接请求,又亦或收到一个暂停信号。所以程序需要循环地等待并处理事件,这就是事件循环。在事件循环中,等待事件一般使用I/O复用技术来实现。I/O框架库一般将系统支持的各种I/O复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是select、poll、cpoll_wait等函数。
此外,事件多路分发器还需要实现register_event和remove_event方法,以供调用者往 事件多路分发器中添加事件和从事件多路分发器中删除事件。
3.事件处理器和具体事件处理器
事件处理器执行事件对应的业务逻辑。它通常包含一个或多个handle_event 回调函数, 这些回调函数在事件循环中被执行。I/O框架库提供的事件处理器通常是一个接口,用户需要继承它来实现自己的事件处理器,即具体事件处理器。因此,事件处理器中的回调函数一般被声明为虚函数,以支持用户的扩展。
此外,事件处理器一般还提供一个get_handle方法,它返回与该事件处理器关联的句柄。那么,事件处理器和句柄有什么关系?当事件多路分发器检测到有事件发生时,它是通 过句柄来通知应用程序的。因此,我们必须将事件处理器和句柄绑定,才能在事件发生时获 取到正确的事件处理器。
4.Reactor
Reactor是I/O框架库的核心。它提供的几个主要方法是:
handle_events。该方法执行事件循环。它重复如下过程:等待事件,然后依次处理所 有就绪事件对应的事件处理器。
register_handler。该方法调用事件多路分发器的register_event方法来往事件多路分发 器中注册一个事件。
remove_handler。该方法调用事件多路分发器的 remove_event 方法来删除事件多路分发器中的一个事件。
12.2 Libevent 源码分析
Libevent是开源社区的一款高性能的I/O框架库,其学习者和使用者众多。使用Libevent的著名案例有:高性能的分布式内存对象缓存软件memcached,Google 浏览器 Chromium的Linux版本。
作为一个I/O框架库,Libevent具有如下特点:
-
跨平台支持。Libevent支持Linux、UNIX和Windows。
-
统一事件源。Libevent对I/O事件、信号和定时事件提供统一的处理。
-
线程安全。Libevent使用 libevent _pthreads 库来提供线程安全支持。
-
基于Reactor模式的实现。
Reactor 模式 是一种常见的事件驱动编程模型,用于构建高性能、并发的软件系统。该模式是基于分发器(Dispatcher)和处理器(Handler)的概念,将事件的处理分离成两个主要组件,以实现非阻塞的事件处理。
核心组件:
-
事件(Event): 表示系统中发生的事情,可以是用户输入、网络连接、定时器到期等。事件可以被分为输入事件、输出事件、定时事件等。
-
Reactor: 是一个事件分发器,负责监视和分发事件。Reactor 模式中的 Reactor 通常是一个循环,不断地检测事件是否发生,并将事件分发给相应的处理器。
-
处理器(Handler): 处理特定类型事件的组件,也称为事件处理器。处理器负责具体的事件处理逻辑,通常是回调函数或方法。
工作流程:
-
注册事件: 程序将感兴趣的事件注册到 Reactor 中,包括读事件、写事件、定时事件等。
-
等待事件: Reactor 不断轮询等待事件的发生。这通常使用系统调用(如
select
、poll
、epoll
、kqueue
等)来实现。 -
事件分发: 一旦事件发生,Reactor 将事件分发给相应的处理器。这可能是调用注册的回调函数、调用特定对象的方法等。
-
处理事件: 处理器执行相应的事件处理逻辑,可能包括读写数据、响应用户请求等。
-
反复循环: 事件处理完成后,Reactor 继续等待下一轮事件的发生,形成一个事件循环。
优点:
-
非阻塞: Reactor 模式通过事件驱动,使得系统可以在等待某个事件的同时继续处理其他事件,提高了系统的并发性和响应性。
-
可伸缩: 通过多路复用等机制,Reactor 模式能够轻松处理大量并发连接,适用于构建高性能的网络应用。
-
简单清晰: Reactor 模式将事件处理分离成两个主要组件,使系统结构清晰、模块化,易于理解和维护。
使用场景:
-
网络编程: Reactor 模式广泛应用于网络编程,用于处理大量并发连接,如 Web 服务器、聊天服务器等。
-
GUI 应用: 在图形用户界面(GUI)应用程序中,Reactor 模式常用于处理用户输入、窗口事件等。
-
实时系统: 对于需要实时响应事件的系统,如实时数据处理、控制系统等,Reactor 模式也是一种合适的选择。
示例代码:
以下是一个简单的 Reactor 模式的示例,使用 Python 的 select
实现:
import select
import socket
class Reactor:
def __init__(self):
self.handlers = {}
def register(self, event_type, handler):
self.handlers[event_type] = handler
def run(self):
while True:
readable, _, _ = select.select(self.handlers.keys(), [], [])
for event_type in readable:
handler = self.handlers[event_type]
handler.handle_event()
class EventHandler:
def handle_event(self):
pass
class SocketHandler(EventHandler):
def __init__(self, sock):
self.sock = sock
def handle_event(self):
data = self.sock.recv(1024)
print(f"Received data: {data.decode()}")
if __name__ == "__main__":
reactor = Reactor()
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(("127.0.0.1", 8080))
server_socket.listen(5)
reactor.register(server_socket, SocketHandler(server_socket))
print("Reactor is running...")
reactor.run()
在上述示例中,Reactor
类负责事件的注册和分发,EventHandler
是事件处理器的基类,而 SocketHandler
是针对 socket 事件的具体处理器。这样的结构使得系统易于扩展和维护。
这一节中我们将简单地研究一下Libevent源代码的主要部分。分析它除了可以更好地学习网络编程外,还有如下好处:
-
学习编写一个产品级的函数库要考虑哪些细节。
-
提高C语言功底。Libevent源码中使用了大量的函数指针,用C语言实现了多态机制,并提供了一些基础数据结构的高效实现,比如双向链表、最小堆等。
Libevent的官方网站是http://libevent.org/,其中提供Libevent源代码的下载,以及学习Libevent框架库的第一手文档,并且源码和文档的更新也较为频繁。笔者写作此书时使用的Libevent版本是该网站于2012年5月3日发布的2.0.19。下载地址:https://github.com/libevent/libevent/releases?page=2,注意新版本和旧版本有很大的不同。这里分析旧版本。
12.2.1 一个实例
分析一款软件的源代码,最简单有效的方式是从使用入手,这样才能从整体上把握该软 件的逻辑结构。代码清单12-1是使用Libevent库实现的一个“Hello World”程序。
12-1libevent_test.c
#include <sys/signal.h>
#include <event.h>
// Signal回调函数,处理中断信号
void signal_cb(int fd, short event, void* argc) {
// 将传递过来的参数还原为event_base指针
struct event_base* base = (event_base*)argc;
// 设置一个2秒的延迟,然后调用event_base_loopexit退出事件循环
struct timeval delay = {2, 0};
printf("Caught an interrupt signal; exiting cleanly in two seconds...\n");
event_base_loopexit(base, &delay);
}
// Timeout回调函数,处理定时器事件
void timeout_cb(int fd, short event, void* argc) {
printf("timeout\n");
}
int main() {
// 初始化event_base
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资源
event_base_free(base);
return 0;
}
这段代码使用libevent库创建了一个事件循环,处理中断信号(SIGINT)和定时器事件。以下是对每个关键函数的注释:
-
void signal_cb(int fd, short event, void* argc)
: 处理中断信号的回调函数。在收到中断信号时,输出一条消息,并设置一个2秒的延迟后退出事件循环。 -
void timeout_cb(int fd, short event, void* argc)
: 处理定时器事件的回调函数。在定时器触发时,输出一条消息。 -
int main()
: 主函数,创建libevent的event_base
对象,初始化中断信号事件和定时器事件,进入事件循环。在事件循环中,等待事件发生并调用相应的回调函数。 -
struct event_base* base = event_init()
: 初始化libevent的event_base
对象,用于管理事件。 -
struct event* signal_event = evsignal_new(base, SIGINT, signal_cb, base)
: 创建一个处理中断信号的事件,并设置回调函数为signal_cb
。 -
event_add(signal_event, NULL)
: 将中断信号事件添加到事件循环中。 -
timeval tv = {1, 0}; struct event* timeout_event = evtimer_new(base, timeout_cb, NULL)
: 创建一个定时器事件,设置回调函数为timeout_cb
,并设置定时器触发时间为1秒。 -
event_add(timeout_event, &tv)
: 将定时器事件添加到事件循环中,并设置触发时间。 -
event_base_dispatch(base)
: 进入libevent的事件循环,等待事件的发生。 -
event_free(timeout_event)
,event_free(signal_event)
: 释放事件资源。 -
event_base_free(base)
: 释放event_base
资源。
代码清单12-1虽然简单,但却基本上描述了Libevent库的主要逻辑:
1)调用event_init 函数创建 event_base对象。一个event_base 相当于一个Reactor实例。
2)创建具体的事件处理器,并设置它们所从属的Reactor实例。evsignal_new和 evtimer_new分别用于创建信号事件处理器和定时事件处理器,它们是定义在include/event2/ event.h 文件中的宏:
#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函数,即用于创建通用事件处理器(图12-1中的EventHandler)的函数。其定义是:
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参数传递文件描述符值;创建信号事件处理器时,应该给fd参数传递信号值,比如代码清单12-1中的SIGINT;创建定时事件处理器时,则应该给fd参数传递-1。events参数指定事件类型,其可选值都定义在 include/event2/event.h 文件中,如代码清单12-2所示。
代码清单12-2中,EV_PERSIST的作用是:事件被触发后,自动重新对这个event调用 event_add 函数(见后文)。
cb参数指定目标事件对应的回调函数,相当于图12-1中事件处理器的handle_event方 法。arg参数则是Reactor传递给回调函数的参数。
event_new函数成功时返回一个event类型的对象,也就是Libevent的事件处理器。 Libevent 用单词“event”来描述事件处理器,而不是事件,会使读者觉得有些混乱,故而我们约定如下:
-
事件指的是一个句柄上绑定的事件,比如文件描述符0上的可读事件。
-
事件处理器,也就是event结构体类型的对象,除了包含事件必须具备的两个要素(句柄和事件类型)外,还有很多其他成员,比如回调函数。
-
事件由事件多路分发器管理,事件处理器则由事件队列管理。事件队列包括多种,比 如event_base中的注册事件队列、活动事件队列和通用定时器队列,以及evmap中 的I/O事件队列、信号事件队列。关于这些事件队列,我们将在后文依次讨论。
-
事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理 器中的回调函数。
3)调用event_add函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应 的事件添加到事件多路分发器中。event_add函数相当于Reactor中的register_handler方法。
4)调用event_base_dispatch 函数来执行事件循环。
5)事件循环结束后,使用*_free系列函数来释放系统资源。
由此可见,代码清单12-1给我们提供了一条分析Libevent源代码的主线。不过在此之前,我们先简单介绍一下Libevent源代码的组织结构。
12.2.2 源代码组织结构
Libevent 源代码中的目录和文件按照功能可划分为如下部分:
- 头文件目录include/event2。该目录是自Libevent主版本升级到2.0之后引入的,在 1.4及更老的版本中并无此目录。该目录中的头文件是Libevent提供给应用程序使用的,比如,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两个结构体的相关操作。
- devpoll.c、kqueue.c、evport.c、select.c、win32select.c、poll.c和epoll.c文件。它们分别封装了如下IV/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.c文件。它提供对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 Procedure Call,远程过程调用)协议的支持。
- epoll _sub.c文件。该文件未见使用。
在整个源码中,event-internal.h、include/event2/event_struct.h、event.c和evmap.c等4个文件最为重要。它们定义了event和event_base结构体,并实现了这两个结构体的相关操作。下面的讨论也主要是围绕这几个文件展开的。
12.2.3 event 结构体
前文提到,Libevent中的事件处理器是event结构类型。event结构体封装了句柄、事件类型、回调函数,以及其他必要的标志和数据。该结构体在include/event2/event_struct.h文 件中定义:
struct event_base;
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 {
// 用于IO事件的结构
struct {
// 在IO事件列表中的条目
TAILQ_ENTRY(event) ev_io_next;
// IO事件的超时
struct timeval ev_timeout;
} ev_io;
// 用于信号事件的结构
struct {
// 在信号事件列表中的条目
TAILQ_ENTRY(event) ev_signal_next;
// 信号事件调用的次数
short ev_ncalls;
// 允许在回调中删除
short* ev_pncalls;
} ev_signal;
} _ev;
// 事件事件(例如,EV_READ,EV_WRITE)
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结构体中的每个成员:
-
ev_events。它代表事件类型。其取值可以是代码清单12-2所示的标志的按位或(互 斥的事件类型除外,比如读写事件和信号事件就不能同时被设置)
-
ev_next。所有已经注册的事件处理器(包括I/O事件处理器和信号事件处理器)通过该成员串联成一个尾队列,我们称之为注册事件队列。宏TAILQ_ENTRY是尾队列 中的节点类型,它定义在compat/sys/queue.h文件中:
#define TAILQ_ENTRY(type) \ struct { \ struct type *tqe_next; /* next element */ \ struct type **tqe_prev; /* address of previous next element */ \ }
-
ev_active_next。所有被激活的事件处理器通过该成员串联成一个尾队列,我们称之为活动事件队列。活动事件队列不止一个,不同优先级的事件处理器被激活后将被插入不同的活动事件队列中。在事件循环中,Reactor将按优先级从高到低遍历所有活动事件队列,并依次处理其中的事件处理器。
-
ev_timeout_pos。这是一个联合体,它仅用于定时事件处理器。为了讨论的方便,后面我们称定时事件处理器为定时器。老版本的Libevent中,定时器都是由时间堆来管理的。但开发者认为有时候使用简单的链表来管理定时器将具有更高的效率。因此, 新版本的Libevent就引入了所谓“通用定时器”的概念。这些定时器不是存储在时间堆中,而是存储在尾队列中,我们称之为通用定时器队列。对于通用定时器而言, ev_timeout_pos 联合体的ev_next_with_common_timeout 成员指出了该定时器在通用定时器队列中的位置。对于其他定时器而言,ev_timeout_pos联合体的min_heap_idx 成员指出了该定时器在时间堆中的位置。一个定时器是否是通用定时器取决于其超时值大小,具体判断原则请读者自己参考event.c文件中的is _common_timeout 函数。
-
_ev。这是一个联合体。所有具有相同文件描述符值的I/O事件处理器通过ev.ev_io.ev_io_next成员串联成一个尾队列,我们称之为I/O事件队列;所有具有相同信号 值的信号事件处理器通过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使用I/O事件队列将具有相同文件描述符值的事件处理器组织在一起。这样,当一个文件描述符上有事件发生时,事件多路分发器就能很快地把所有相关的事件处理器添加到活动事件队列中。信号事件队列的存在也是由于相同的原因。可见,I/O事件队列和信号事件队列并不是注册事件队列的细致分类,而是另有用处。
-
ev_fd。对于L/O事件处理器,它是文件描述符值;对于信号事件处理器,它是信号值。
-
ev_base。该事件处理器从属的event base实例。
-
ev_res。它记录当前激活事件的类型。
-
ev_flags。它是一些事件标志。其可选值定义在include/event2/event_struct.h文件中:
// 标志位,事件处理器从属于通用定时器队列或时间堆 #define EVLIST_TIMEOUT 0x01 // 标志位,表示事件在插入列表中 #define EVLIST_INSERTED 0x02 // 标志位,表示事件是信号事件 #define EVLIST_SIGNAL 0x04 // 标志位,表示事件在活动列表中 #define EVLIST_ACTIVE 0x08 // 标志位,表示事件是内部事件 #define EVLIST_INTERNAL 0x10 // 标志位,表示事件已初始化 #define EVLIST_INIT 0x80 // EVLIST_X_ 私有空间:0x1000-0xf000 #define EVLIST_ALL (0xf000 | 0x9f)
-
ev_pri。它指定事件处理器优先级,值越小则优先级越高。
-
ev_closure。它指定 event_base 执行事件处理器的回调函数时的行为。其可选值定义 于event-internal.h文件中:
/*默认行为*/ #define EV_CLOSURE NONE 0 /*执行信号事件处理器的回调函数时,调用ev.ev_signal.ev_ncal1s次该回调函数*/ #define EV_CL,OSURE SIGNAL 1 /*执行完回调函数后,再次将事件处理器加入注册事件队列中*/ #define EV_CLOSURE_PERSIST 2
-
ev_timeout。它仅对定时器有效,指定定时器的超时值。
-
ev_callback。它是事件处理器的回调函数,由event_base 调用。回调函数被调 用时,它的3个参数分别被传人事件处理器的如下3个成员:ev_fd、ev_res和ev_arg。
-
ev_arg。回调函数的参数。
12.2.4 往注册事件队列中添加事件处理器
前面提到,创建一个event对象的函数是event_new(及其变体),它在event.c文件中实现。该函数的实现相当简单,主要是给event对象分配内存并初始化它的部分成员,因此我们不讨论它。event对象创建好之后,应用程序需要调用event _add 函数将其添加到注册事件队列中,并将对应的事件注册到事件多路分发器上。event_add函数在event.c文件中实现, 主要是调用另外一个内部函数event_add_internal, 如代码清单12-3所示:
/*
* 该函数是向事件库中添加事件的实现函数,与 event_add 函数类似,
* 不同之处在于:
* 1. 它要求我们已经获取了锁。
* 2. 如果 tv_is_absolute 被设置,它将 tv 视为绝对时间,而不是添加到当前时间的间隔。
*/
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 res = 0;
int notify = 0;
// 断言确保在合适的环境中调用该函数
EVENT_BASE_ASSERT_LOCKED(base);
// 事件调试信息输出
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));
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); /* ENOMEM == errno */
}
// 如果主线程当前正在执行信号事件的回调,并且我们不是主线程,则等待回调完成
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
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))) {
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) {
/* evmap 告诉我们需要通知主线程 */
notify = 1;
res = 0;
}
}
/*
* 只有在上一个事件添加成功的情况下,我们应该更改超时状态。
*/
if (res != -1 && tv != NULL) {
struct timeval now;
int common_timeout;
/*
* 对于持续的超时事件,我们记住超时值并重新添加事件。
*
* 如果 tv_is_absolute 已经设置,这个值已经设置过了。
*/
if (ev->ev_closure == EV_CLOSURE_PERSIST && !tv_is_absolute)
ev->ev_io_timeout = *tv;
/*
* 我们在上面已经为不替换现有超时的情况保留了内存。
*/
if (ev->ev_flags & EVLIST_TIMEOUT) {
/* XXX 我相信这是不需要的。*/
if (min_heap_elt_is_top(ev))
notify = 1;
event_queue_remove(base, ev, EVLIST_TIMEOUT);
}
/* 检查它是否因为超时而处于活动状态。在执行回调之前重新调度此超时会将其从活动列表中删除。 */
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;
tmp.tv_usec &= MICROSECONDS_MASK;
evutil_timeradd(&now, &tmp, &ev->ev_timeout);
ev->ev_timeout.tv_usec |=
(tv->tv_usec & ~MICROSE
CONDS_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);
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);
return (res);
}
作用:
- 该函数是向事件库中添加事件的实现函数,用于添加读、写、信号事件。
- 如果是超时事件,根据超时时间插入到超时列表中。
- 如果是持续性的超时事件,会记住超时值并重新添加事件。
- 如果添加成功,会触发通知主线程的标志,唤醒事件循环。
- 如果是信号事件,当主线程正在执行信号事件的回调时,等待回调完成后再处理。
从代码清单12-3可见,event_add_internal 函数内部调用了几个重要的函数:
evmap_io_add。该函数将I/O事件添加到事件多路分发器中,并将对应的事件处理器添加到I/O事件队列中,同时建立I/O事件和I/O事件处理器之间的映射关系。我们将在下一节详细讨论该函数。
evmap_signal_add。该函数将信号事件添加到事件多路分发器中,并将对应的事 件处理器添加到信号事件队列中,同时建立信号事件和信号事件处理器之间的映 射关系。
event_queue_insert。该函数将事件处理器添加到各种事件队列中:将I/O事件处理器和信号事件处理器插人注册事件队列;将定时器插入通用定时器队列或时间堆;将被激活的事件处理器添加到活动事件队列中。其实现如代码清单12-4 所示:
/*
* 向事件库的队列中插入事件的函数,根据队列类型选择插入的方式。
*/
static void
event_queue_insert(struct event_base *base, struct event *ev, int queue)
{
// 断言确保在合适的环境中调用该函数
EVENT_BASE_ASSERT_LOCKED(base);
// 如果事件已经在队列中,对于活动事件,允许双重插入
if (ev->ev_flags & queue) {
if (queue & EVLIST_ACTIVE)
return;
event_errx(1, "%s: %p(fd %d) already on queue %x", __func__,
ev, ev->ev_fd, queue);
return;
}
// 如果事件不在内部队列中,增加事件计数
if (~ev->ev_flags & EVLIST_INTERNAL)
base->event_count++;
// 设置事件在指定队列上的标志
ev->ev_flags |= queue;
// 根据队列类型选择插入的方式
switch (queue) {
case EVLIST_INSERTED:
// 插入到待处理事件队列末尾
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);
}
}
作用:
- 该函数用于向事件库的不同队列中插入事件。
- 根据队列类型选择插入的方式,包括待处理事件队列、活动事件队列、超时事件队列。
- 对于活动事件队列,会增加活动事件计数。
- 对于超时事件队列,根据超时时间选择插入的方式,如果是常规超时,按顺序插入到超时列表中,否则使用最小堆。
12.2.5往事件多路分发器中注册事件
event_queue_insert 函数所做的仅仅是将一个事件处理器加人event_base的某个事件队列 中。对于新添加的I/O事件处理器和信号事件处理器,我们还需要让事件多路分发器来监听其对应的事件,同时建立文件描述符、信号值与事件处理器之间的映射关系。这就要通过调 用evmap_io_add和evmap_signal_add两个函数来完成。这两个函数相当于事件多路分发器 中的register_event方法,它们由evmap.c文件实现。
/**
* 用于evmap_io列表的条目:记录所有希望在给定fd上进行读取或写入的事件以及每种事件的数量。
*/
struct evmap_io {
struct event_list events; // 事件列表
ev_uint16_t nread; // 读事件数量
ev_uint16_t nwrite; // 写事件数量
};
/*
* 用于evmap_signal列表的条目:记录所有希望在信号触发时得知的事件。
*/
struct evmap_signal {
struct event_list events; // 事件列表
};
/*
* 在某些平台上,fds从0开始逐个递增分配,并且旧编号被重复使用。
* 对于这些平台,我们实现io映射与信号映射相同:作为指向struct evmap_io的指针数组。
* 但在其他平台上(如Windows),套接字不是从0开始索引,不一定是连续的,也不一定被重复使用。
* 在这种情况下,我们使用散列表来实现evmap_io。
*/
#ifdef EVMAP_USE_HT
struct event_map_entry {
HT_ENTRY(event_map_entry) map_node; // 散列表节点
evutil_socket_t fd; // 文件描述符
union {
/* 这是一个联合体,以防需要创建更多可以放入散列表的内容。 */
struct evmap_io evmap_io; // io映射
} ent;
};
#endif
由于evmap_io_add和evmap_signal_add两个函数的逻辑基本相同,因此我们仅讨论 evmap_io_add 函数,如代码清单12-6所示:
/*
* 在事件映射的IO列表中添加事件。
* 返回值:-1 表示错误,0 表示在事件后端没有发生变化,1 表示有变化。
*/
int
evmap_io_add(struct event_base *base, evutil_socket_t fd, struct event *ev)
{
const struct eventop *evsel = base->evsel; // 事件操作对象
struct event_io_map *io = &base->io; // 事件IO映射
struct evmap_io *ctx = NULL; // IO映射上下文
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
if (fd >= io->nentries) {
// 如果文件描述符大于等于映射大小,则调整映射大小
if (evmap_make_space(io, fd, sizeof(struct evmap_io *)) == -1)
return (-1);
}
#endif
// 获取或创建IO映射上下文
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;
}
// 检查读写事件数量是否超过最大值
if (EVUTIL_UNLIKELY(nread > 0xffff || nwrite > 0xffff)) {
event_warnx("Too many events reading or writing on fd %d",
(int)fd);
return -1;
}
// 在调试模式下检查事件是否混合了边缘触发和非边缘触发
if (EVENT_DEBUG_MODE_IS_ON() &&
(old_ev = TAILQ_FIRST(&ctx->events)) &&
(old_ev->ev_events & EV_ET) != (ev->ev_events & EV_ET)) {
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);
// 添加事件到事件后端
if (evsel->add(base, ev->ev_fd,
old, (ev->ev_events & EV_ET) | res, extra) == -1)
return (-1);
retval = 1; // 设置返回值为1,表示事件后端发生变化
}
ctx->nread = (ev_uint16_t)nread; // 更新读事件数量
ctx->nwrite = (ev_uint16_t)nwrite; // 更新写事件数量
TAILQ_INSERT_TAIL(&ctx->events, ev, ev_io_next); // 将事件插入IO映射列表
return (retval);
}
12.2.6 eventop 结构体
eventop结构体封装了I/O复用机制必要的一些操作,比如注册事件、等待事件等。它 为event_base支持的所有后端I/O复用机制提供了一个统一的接口。该结构体定义在event-internal.h 文件中,如代码清单12-7所示。
前文提到,devpoll.c、kqueue.c、evport.c、select.c、win32select.c、poll.c和epoll.c文件分别针对不同的I/O复用技术实现了eventop定义的这套接口。那么,在支持多种I/O复用技术的系统上,Libevent将选择使用哪个呢?这取决于这些I/O复用技术的优先级。Libevent支持的后端I/O复用技术及它们的优先级在event.c文件中定义,如代码清单12-8所示。
#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
/* Array of backends in order of preference. */
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数组来选择其后端I/O复用技术。遍历的顺序是从数组的第一个元素开始,到最后一个元素结束。所以,在Linux下,Libevent 默认选择的后端I/O复用技术是epoll。但很显然,用户可以修改代码清单12-8中定义的一系列宏来选择使用不同的后端I/O复用技术。
12.2.7 event_base结构体
结构体event_base是Libevent的Reactor。它定义在event-intermal.h文件中,如代码清 单12-9所示:
struct event_base {
/** Function pointers and other data to describe this event_base's
* backend. */
const struct eventop *evsel;
/** Pointer to backend-specific data. */
void *evbase;
/** List of changes to tell backend about at next dispatch. Only used
* by the O(1) backends. */
struct event_changelist changelist;
/** Function pointers used to describe the backend that this event_base
* uses for signals */
const struct eventop *evsigsel;
/** Data to implement the common signal handelr code. */
struct evsig_info sig;
/** Number of virtual events */
int virtual_event_count;
/** Number of total events added to this event_base */
int event_count;
/** Number of total events active in this event_base */
int event_count_active;
/** Set if we should terminate the loop once we're done processing
* events. */
int event_gotterm;
/** Set if we should terminate the loop immediately */
int event_break;
/** Set if we should start a new instance of the loop immediately. */
int event_continue;
/** The currently running priority of events */
int event_running_priority;
/** Set if we're running the event_base_loop function, to prevent
* reentrant invocation. */
int running_loop;
/* Active event management. */
/** An array of nactivequeues queues for active events (ones that
* have triggered, and whose callbacks need to be called). Low
* priority numbers are more important, and stall higher ones.
*/
struct event_list *activequeues;
/** The length of the activequeues array */
int nactivequeues;
/* common timeout logic */
/** An array of common_timeout_list* for all of the common timeout
* values we know. */
struct common_timeout_list **common_timeout_queues;
/** The number of entries used in common_timeout_queues */
int n_common_timeouts;
/** The total size of common_timeout_queues. */
int n_common_timeouts_allocated;
/** List of defered_cb that are active. We run these after the active
* events. */
struct deferred_cb_queue defer_queue;
/** Mapping from file descriptors to enabled (added) events */
struct event_io_map io;
/** Mapping from signal numbers to enabled (added) events. */
struct event_signal_map sigmap;
/** All events that have been enabled (added) in this event_base */
struct event_list eventqueue;
/** Stored timeval; used to detect when time is running backwards. */
struct timeval event_tv;
/** Priority queue of events with timeouts. */
struct min_heap timeheap;
/** Stored timeval: used to avoid calling gettimeofday/clock_gettime
* too often. */
struct timeval tv_cache;
#if defined(_EVENT_HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
/** Difference between internal time (maybe from clock_gettime) and
* gettimeofday. */
struct timeval tv_clock_diff;
/** Second in which we last updated tv_clock_diff, in monotonic time. */
time_t last_updated_clock_diff;
#endif
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
/* threading support */
/** The thread currently running the event_loop for this base */
unsigned long th_owner_id;
/** A lock to prevent conflicting accesses to this event_base */
void *th_base_lock;
/** The event whose callback is executing right now */
struct event *current_event;
/** A condition that gets signalled when we're done processing an
* event with waiters on it. */
void *current_event_cond;
/** Number of threads blocking on current_event_cond. */
int current_event_waiters;
#endif
#ifdef WIN32
/** IOCP support structure, if IOCP is enabled. */
struct event_iocp_port *iocp;
#endif
/** Flags that this base was configured with */
enum event_base_config_flag flags;
/* Notify main thread to wake up break, etc. */
/** True if the base already has a pending notify, and we don't need
* to add any more. */
int is_notify_pending;
/** A socketpair used by some th_notify functions to wake up the main
* thread. */
evutil_socket_t th_notify_fd[2];
/** An event used by some th_notify functions to wake up the main
* thread. */
struct event th_notify;
/** A function used to wake up the main thread from another thread. */
int (*th_notify_fn)(struct event_base *base);
};
用中文注解如下:
/*
* 事件库的基本结构体,描述事件库的配置和状态。
*/
struct event_base {
/** 事件库使用的后端的函数指针和其他数据。 */
const struct eventop *evsel;
/** 后端特定数据的指针。 */
void *evbase;
/** 下一次调度时要通知后端的更改列表。仅由O(1)后端使用。 */
struct event_changelist changelist;
/** 用于描述事件库用于信号处理的后端的函数指针。*/
const struct eventop *evsigsel;
/** 实现通用信号处理器代码的数据。 */
struct evsig_info sig;
/** 虚拟事件的数量 */
int virtual_event_count;
/** 添加到此事件库的总事件数量 */
int event_count;
/** 在此事件库中处于活动状态的总事件数量 */
int event_count_active;
/** 如果我们完成处理事件后应该终止循环,则设置此标志。 */
int event_gotterm;
/** 如果应立即终止循环,则设置此标志。 */
int event_break;
/** 如果应立即启动新的循环实例,则设置此标志。 */
int event_continue;
/** 当前运行的事件的优先级 */
int event_running_priority;
/** 设置为正在运行event_base_loop函数,以防止重入调用。 */
int running_loop;
/* 活动事件管理。 */
/** 用于活动事件(已触发并且需要调用其回调函数的事件)的nactivequeues队列数组。
* 低优先级数字更重要,而高优先级则停顿。 */
struct event_list *activequeues;
/** activequeues数组的长度 */
int nactivequeues;
/* 通用超时逻辑 */
/** 所有我们知道的通用超时值的common_timeout_list*数组。 */
struct common_timeout_list **common_timeout_queues;
/** 在common_timeout_queues中使用的条目数 */
int n_common_timeouts;
/** common_timeout_queues的总大小。 */
int n_common_timeouts_allocated;
/** 活动的延迟回调队列。我们在处理活动事件后运行这些回调。 */
struct deferred_cb_queue defer_queue;
/** 文件描述符到启用(添加)事件的映射 */
struct event_io_map io;
/** 信号号到启用(添加)事件的映射。 */
struct event_signal_map sigmap;
/** 所有在此event_base中启用(添加)的事件的列表。 */
struct event_list eventqueue;
/** 存储的时间值;用于检测时间是否倒退。 */
struct timeval event_tv;
/** 具有超时的事件的优先级队列。 */
struct min_heap timeheap;
/** 存储的时间值:用于避免过于频繁地调用gettimeofday/clock_gettime。 */
struct timeval tv_cache;
#if defined(_EVENT_HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
/** 内部时间(可能来自clock_gettime)和gettimeofday之间的差异。 */
struct timeval tv_clock_diff;
/** 我们上次更新tv_clock_diff时的单调时间。 */
time_t last_updated_clock_diff;
#endif
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
/* 线程支持 */
/** 当前运行此事件库的事件循环的线程 */
unsigned long th_owner_id;
/** 锁,以防止对此事件库的冲突访问。 */
void *th_base_lock;
/** 当前正在执行回调的事件。 */
struct event *current_event;
/** 在其上有等待的事件时通知我们完成处理事件的条件。 */
void *current_event_cond;
/** 在current_event_cond上等待的线程数。 */
int current_event_waiters;
#endif
#ifdef WIN32
/** IOCP支持结构,如果启用了IOCP。 */
struct event_iocp_port *iocp;
#endif
/** 配置此基础的标志。 */
enum event_base_config_flag flags;
/* 通知主线程唤醒中断等。 */
/** 如果基础已经有待处理的通知,则为true,我们无需添加更多。 */
int is_notify_pending;
/** 由一些th_notify函数使用的socketpair,用于唤醒主线程。 */
evutil_socket_t th_notify_fd[2];
/** 由一些th_notify函数使用的事件,用于唤醒主线程。 */
struct event th_notify;
/** 由一些th_notify函数使用的函数,用于从另一个线程唤醒主线程。 */
int (*th_notify_fn)(struct event_base *base);
};
作用: 上述代码定义了事件库的基本结构体,用于描述事件库的配置和状态。结构体中包含了事件库的后端信息、活动事件管理、通用超时逻辑、线程支持、IOCP支持等多个成员,以及用于通知主线程唤醒中断等的相关信息。该结构体是事件库内部数据结构的主要容器,用于维护事件库的状态和配置信息。
12.2.8 事件循环
最后,我们讨论一下Libevent的“动力”,即事件循环。Libevent中实现事件循环的函 数是event_base_loop(在event.c文件中)。该函数首先调用I/O事件多路分发器的事件监听函数,以等待事件; 当有事件发生时,就依次处理之。event_base_loop函数的实现如代码清单12-10所示:
/*
* 运行事件库的主事件循环。
* 参数:
* - base: 要运行事件循环的事件库。
* - flags: 控制事件循环行为的标志。
* 返回值:
* - 成功返回0,失败返回-1。
*/
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;
/* 获取锁。我们将在evsel.dispatch内部释放它,以及在调用用户回调时再次释放。 */
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
// 检查是否有其他循环正在运行
if (base->running_loop) {
event_warnx("%s: 重入调用。每个event_base只能同时运行一个event_base_loop。", __func__);
EVBASE_RELEASE_LOCK(base, th_base_lock);
return -1;
}
base->running_loop = 1; // 设置循环状态为运行中
clear_time_cache(base); // 清除时间缓存
// 如果有信号事件被添加,并且信号计数不为零,则设置信号相关的信息
if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)
evsig_set_base(base);
done = 0;
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
base->th_owner_id = EVTHREAD_GET_ID(); // 获取当前线程的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;
}
timeout_correct(base, &tv); // 纠正超时
tv_p = &tv;
// 如果没有活动事件且非阻塞标志未设置,则计算下一个超时时间
if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);
} else {
/*
* 如果有活动事件,我们只是轮询新事件而不等待。
*/
evutil_timerclear(&tv);
}
/* 如果没有事件,我们就退出 */
if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
event_debug(("%s: 未注册事件。", __func__));
retval = 1;
goto done;
}
/* 更新最后的旧时间 */
gettime(base, &base->event_tv);
clear_time_cache(base); // 清除时间缓存
res = evsel->dispatch(base, tv_p); // 调用具体事件处理的dispatch函数
if (res == -1) {
event_debug(("%s: dispatch 返回失败。", __func__));
retval = -1;
goto done;
}
update_time_cache(base); // 更新时间缓存
timeout_process(base); // 处理超时事件
if (N_ACTIVE_CALLBACKS(base)) {
int n = event_process_active(base); // 处理活动事件
// 如果是一次性循环并且没有活动事件了,或者非一次性循环并且有活动事件,则设置done为1
if ((flags & EVLOOP_ONCE) && N_ACTIVE_CALLBACKS(base) == 0 && n != 0)
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
event_debug(("%s: 被要求终止循环。", __func__));
done:
clear_time_cache(base); // 清除时间缓存
base->running_loop = 0; // 设置循环状态为非运行中
EVBASE_RELEASE_LOCK(base, th_base_lock); // 释放锁
return (retval);
}
done:
是标签(label)语法。在C语言中,标签语法可以用于在嵌套循环或者嵌套语句中提供一个位置,以便在后续代码中通过 goto
语句跳转到这个位置。
在上述代码中,done:
标签标记了一段代码块的结束位置。在 goto done;
语句中,程序会跳转到标签 done:
标记的位置,从而结束循环并执行 done
标签后面的代码。这种用法可以在某些情况下简化代码结构,但过度使用 goto
和标签可能会导致代码难以理解和维护,因此需要谨慎使用。
后记
现在libevent的最新版本下载地址为:https://github.com/libevent/libevent,是这个版本:release-2.1.12-stable。学习到这一章,对libevent库有了初步的认识。