libevent(2)之使用教程(1)介绍
Libevent(2)之使用教程(1)介绍
Author: Once Day Date: 2025年6月29日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
本文档翻译于:Fast portable non-blocking network programming with Libevent
全系列文章可参考专栏: 十年代码训练_Once-Day的博客-CSDN博客
参考文章:
- 详解libevent网络库(一)—框架的搭建_libevent详解-CSDN博客
- 深度思考高性能网络库Libevent,从13个维度来解析Libevent到底是怎么回事 - 知乎
- 深入浅出理解libevent——2万字总结_libev 堆-CSDN博客
- Fast portable non-blocking network programming with Libevent
- libevent
- C++网络库:Libevent网络库的原理及使用方法 - 知乎
- 深入理解libevent事件库的原理与实践技巧-腾讯云开发者社区-腾讯云
文章目录
- Libevent(2)之使用教程(1)介绍
- 1. 异步IO介绍
- 2. libevent索引
- 3. 配置libevent
- 3.1 Libevent 中的日志消息
- 3.2 处理致命错误
- 3.3 内存管理
- 3.4 线程间互斥
- 3.5 调试锁使用
- 3.6 调试事件对象
- 3.7 检查libevent版本
- 3.8 释放全局libevent数据
1. 异步IO介绍
原文章节链接:wangafu.net/~nickm/libevent-book/01_intro.html
大多数初学编程者都从阻塞式 IO 调用开始。如果一个 IO 调用在被调用时,直到操作完成或网络栈因超时放弃前都不会返回,那么该 IO 调用就是同步的。例如,当你在 TCP 连接上调用 connect ()
时,操作系统会将一个 SYN 数据包排入队列,发往 TCP 连接另一端的主机。直到它收到对端主机的 SYN ACK 数据包,或者因超时而决定放弃,才会将控制权交还给应用程序。
下面是一个使用阻塞式网络调用的简单客户端示例。它会打开与www.baidu.com
的连接,发送一个简单的 HTTP 请求,并将响应打印到标准输出。
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For gethostbyname */
#include <netdb.h>#include <unistd.h>
#include <string.h>
#include <stdio.h>int main(int c, char **v)
{const char query[] ="GET / HTTP/1.0\r\n""Host: www.baidu.com\r\n""\r\n";const char hostname[] = "www.baidu.com";struct sockaddr_in sin;struct hostent *h;const char *cp;int fd;ssize_t n_written, remaining;char buf[1024];/* Look up the IP address for the hostname. Watch out; this isn'tthreadsafe on most platforms. */h = gethostbyname(hostname);if (!h) {fprintf(stderr, "Couldn't lookup %s: %s", hostname, hstrerror(h_errno));return 1;}if (h->h_addrtype != AF_INET) {fprintf(stderr, "No ipv6 support, sorry.");return 1;}/* Allocate a new socket */fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("socket");return 1;}/* Connect to the remote host. */sin.sin_family = AF_INET;sin.sin_port = htons(80);sin.sin_addr = *(struct in_addr *)h->h_addr;if (connect(fd, (struct sockaddr *)&sin, sizeof(sin))) {perror("connect");close(fd);return 1;}/* Write the query. *//* XXX Can send succeed partially? */cp = query;remaining = strlen(query);while (remaining) {n_written = send(fd, cp, remaining, 0);if (n_written <= 0) {perror("send");return 1;}remaining -= n_written;cp += n_written;}/* Get an answer back. */while (1) {ssize_t result = recv(fd, buf, sizeof(buf), 0);if (result == 0) {break;} else if (result < 0) {perror("recv");close(fd);return 1;}fwrite(buf, 1, result, stdout);}close(fd);return 0;
}
上述代码中的所有网络调用都是阻塞的:gethostbyname
会一直等待,直到成功解析 www.google.com
或解析失败才会返回;connect
会一直阻塞,直到连接成功建立;recv
会一直等待,直到接收到数据或连接关闭;send
至少会等到将输出刷新到内核的写缓冲区后才会返回。
阻塞 IO 本身并非 “洪水猛兽”。如果程序在等待期间没有其他任务需要处理,那么阻塞 IO 完全可以满足需求。但假设你需要编写一个程序来同时处理多个连接,比如要从两个连接读取输入,且无法预知哪个连接会先有数据到达。此时,你不能简单地这样写:
/* This won't work. */
char buf[1024];
int i, n;
while (i_still_want_to_read()) {for (i=0; i<n_sockets; ++i) {n = recv(fd[i], buf, sizeof(buf), 0);if (n==0)handle_close(fd[i]);else if (n<0)handle_error(fd[i], errno);elsehandle_input(fd[i], buf, n);}
}
因为如果数据首先到达文件描述符 fd [2],你的程序在从 fd [0] 和 fd [1] 读取到数据并完成操作之前,甚至不会尝试从 fd [2] 读取数据。
有时人们会通过多线程或多进程服务器来解决这个问题。实现多线程的最简单方法之一是为每个连接分配独立的进程(或线程)来处理。由于每个连接都有专属进程,等待某个连接的阻塞 IO 调用不会导致其他连接的进程被阻塞。
下面是另一个示例程序:这是一个简单的服务器,监听 TCP 端口 40713,逐行读取输入数据,并在数据到达时输出每行的 ROT13 加密结果。它使用 Unix 系统的 fork () 调用为每个传入连接创建新进程。
while (1) {struct sockaddr_storage ss;socklen_t slen = sizeof(ss);int fd = accept(listener, (struct sockaddr *)&ss, &slen);if (fd < 0) {perror("accept");} else {if (fork() == 0) {child(fd);exit(0);}}
}
那么,我们是否拥有同时处理多个连接的完美解决方案呢?我能否就此停笔去做其他事情了呢?并非如此。首先,在某些平台上,进程创建(甚至线程创建)的开销可能非常大。在实际场景中,你会希望使用线程池而非创建新进程。但更根本的问题是,线程的扩展性无法达到预期 —— 如果程序需要同时处理数千甚至数万个连接,管理数万个线程的效率远不如让每个 CPU 仅处理少量线程。
如果线程并非处理多连接的答案,那什么才是呢?在 Unix 设计范式中,解决方案是将套接字设置为非阻塞模式。Unix 中实现这一操作的调用如下:
fcntl(fd, F_SETFL, O_NONBLOCK);
其中fd
是套接字的文件描述符。一旦将fd
(套接字)设置为非阻塞模式,之后对该fd
的任何网络调用要么立即完成操作,要么返回特定错误码以表明 “当前无法推进操作,请重试”。因此,之前的双套接字示例可能会被简单地写成:
/* This will work, but the performance will be unforgivably bad. */
int i, n;
char buf[1024];
for (i=0; i < n_sockets; ++i)fcntl(fd[i], F_SETFL, O_NONBLOCK);while (i_still_want_to_read()) {for (i=0; i < n_sockets; ++i) {n = recv(fd[i], buf, sizeof(buf), 0);if (n == 0) {handle_close(fd[i]);} else if (n < 0) {if (errno == EAGAIN); /* The kernel didn't have any data for us to read. */elsehandle_error(fd[i], errno);} else {handle_input(fd[i], buf, n);}}
}
现在我们使用了非阻塞套接字,上述代码确实能运行…… 但仅仅是勉强运行,其性能会非常糟糕,原因有两点。其一,当两个连接都没有数据可读时,循环会无限空转,耗尽所有 CPU 周期。其二,若尝试用这种方式处理超过一两个连接,无论每个连接是否有数据,都需要对每个连接进行一次内核调用。因此,我们需要一种方法来告诉内核:“等待这些套接字中的任意一个准备好向我提供数据,并告诉我哪些已准备就绪。”
针对这个问题,人们仍在使用的最古老解决方案是select()
。select()
调用接受三组文件描述符(以位阵列实现):一组用于读取,一组用于写入,一组用于 “异常”。它会一直等待,直到其中一组中的某个套接字准备就绪,并修改这些集合,使其仅包含准备好使用的套接字。
下面是使用select()
重写的示例代码:
/* If you only have a couple dozen fds, this version won't be awful */
fd_set readset;
int i, n;
char buf[1024];while (i_still_want_to_read()) {int maxfd = -1;FD_ZERO(&readset);/* Add all of the interesting fds to readset */for (i=0; i < n_sockets; ++i) {if (fd[i]>maxfd) maxfd = fd[i];FD_SET(fd[i], &readset);}/* Wait until one or more fds are ready to read */select(maxfd+1, &readset, NULL, NULL, NULL);/* Process all of the fds that are still set in readset */for (i=0; i < n_sockets; ++i) {if (FD_ISSET(fd[i], &readset)) {n = recv(fd[i], buf, sizeof(buf), 0);if (n == 0) {handle_close(fd[i]);} else if (n < 0) {if (errno == EAGAIN); /* The kernel didn't have any data for us to read. */elsehandle_error(fd[i], errno);} else {handle_input(fd[i], buf, n);}}}
}
但我们仍未解决所有问题。由于生成和读取 select ()
位阵列的时间与传递给 select ()
的最大文件描述符成正比,当套接字数量较多时,select ()
调用的扩展性会变得极差。[2]
不同操作系统为 select ()
提供了不同的替代函数,包括 poll ()
、epoll ()
、kqueue ()
、evports
和 /dev/poll
。这些函数的性能均优于 select ()
,且除了 poll ()
之外,其他函数在添加套接字、移除套接字以及检测套接字是否准备好进行 IO 操作时均能提供 O (1) 级性能。
遗憾的是,这些高效接口都未成为通用标准:Linux 提供了 epoll ()
,BSD 系统(包括 Darwin)提供了 kqueue ()
,Solaris 提供了 evports
和 /dev/poll
…… 而这些操作系统彼此之间并不兼容。因此,如果需要编写可移植的高性能异步应用程序,就需要一个抽象层来封装所有这些接口,并根据运行环境选择最高效的实现。
这正是 Libevent API 底层为开发者所做的工作:它为各种 select () 替代方案提供了统一接口,并会在运行时自动选择当前计算机上最高效的实现方式。
你可能已经注意到,随着代码效率的提升,其复杂度也在增加。在使用多进程(fork)方案时,我们无需为每个连接单独管理缓冲区 —— 每个进程都有独立的栈分配缓冲区;也无需显式跟踪每个套接字是处于读取还是写入状态,这些状态隐含在代码执行流程中;更不需要额外的数据结构来记录每个操作的完成进度,只需使用循环和栈变量即可。
此外,如果你对 Windows 网络编程有深入了解,就会意识到上述示例中的 Libevent 实现可能无法在 Windows 平台发挥最佳性能。在 Windows 上,实现高性能异步 IO 的正确方式并非使用类 select () 接口,而是通过 IOCP(IO 完成端口)API。与其他高性能网络 API 不同,IOCP 不会在套接字准备好执行某个操作时通知程序,而是由程序主动告知 Windows 网络栈启动一个网络操作,待操作完成后,IOCP 再通知程序。
幸运的是,Libevent 2 的 “bufferevents” 接口同时解决了这两个问题:它显著简化了程序编写难度,并提供了一个可在 Windows 和 Unix 系统上均实现高效的统一接口。
2. libevent索引
原文章节链接:wangafu.net/~nickm/libevent-book/Ref0_meta.html
Libevent 是用于开发快速可移植非阻塞 IO 程序的库,其设计目标包括:
- 可移植性:使用 Libevent 编写的程序应能在所有 Libevent 支持的平台上运行。即便在没有理想非阻塞 IO 实现的情况下,Libevent 也应支持次优方案,确保程序能在受限环境中运行。
- 速度:Libevent 会尝试在各平台上使用可用的最快非阻塞 IO 实现,且尽量不引入过多开销。
- 可扩展性:Libevent 的设计即使在程序需要管理数万个活跃套接字时也能良好工作。
- 易用性:在可能的情况下,使用 Libevent 编写程序的最自然方式应是稳定且可移植的方式。
Libevent 的组成部分:
- evutil:用于抽象不同平台网络实现差异的通用功能。
- event 和 event_base:这是 Libevent 的核心,为各种基于平台特定事件的非阻塞 IO 后端提供抽象 API。它能告知套接字何时可读取或写入,实现基本的超时功能,并检测操作系统信号。
- bufferevent:这些函数为 Libevent 基于事件的核心提供了更便捷的包装。它们允许应用程序请求带缓冲的读写操作,不是在套接字准备好时通知,而是在 IO 实际发生时通知。bufferevent 接口也有多个后端,因此能利用提供更快非阻塞 IO 方式的系统,如 Windows IOCP API。
- evbuffer:该模块实现了 bufferevent 底层的缓冲区,并提供高效和 / 或便捷的访问函数。
- evhttp:简单的 HTTP 客户端 / 服务器实现。
- evdns:简单的 DNS 客户端 / 服务器实现。
- evrpc:简单的 RPC 实现。
Libevent 构建时,默认安装以下库:
- libevent_core:包含所有核心事件和缓冲区功能,该库包含所有 event_base、evbuffer、bufferevent 和实用函数。
- libevent_extra:该库定义了应用程序可能需要也可能不需要的特定协议功能,包括 HTTP、DNS 和 RPC。
- libevent:该库因历史原因存在,包含 libevent_core 和 libevent_extra 的内容,不应使用,未来版本的 Libevent 可能会移除它。
以下库仅在某些平台上安装:
- libevent_pthreads:该库添加了基于 pthreads 可移植线程库的线程和锁定实现。它与 libevent_core 分离,因此除非实际在多线程环境中使用 Libevent,否则无需链接 pthreads。
- libevent_openssl:该库使用 bufferevent 和 OpenSSL 库提供加密通信支持。它与 libevent_core 分离,因此除非实际使用加密连接,否则无需链接 OpenSSL。
所有当前公共 Libevent 头文件都安装在 event2 目录下。头文件分为三大类:
- API 头文件:定义 Libevent 当前公共接口的头文件,这些头文件没有特殊后缀。
- 兼容性头文件:包含已弃用函数的定义,除非从旧版本 Libevent 移植程序,否则不应包含。
- 结构头文件:定义布局相对易变的结构。其中一些结构被公开是因为需要快速访问结构组件,一些是出于历史原因。直接依赖头文件中的任何结构可能会破坏程序与其他版本 Libevent 的二进制兼容性,有时会以难以调试的方式。这些头文件有后缀 “_struct.h”。
ibevent 2.0 已修订其 API,使其更合理且不易出错。如果可能,应编写新程序使用 Libevent 2.0 API。在 Libevent 2.0 及更高版本中,旧头文件仍作为新头文件的包装存在。
使用旧版本的其他注意事项:
- 在 1.4 版本之前,只有一个库 “libevent”,包含当前拆分为 libevent_core 和 libevent_extra 的功能。
- 在 2.0 版本之前,不支持锁定;Libevent 可以是线程安全的,但前提是确保从不同时在两个线程中使用同一结构。
3. 配置libevent
原文链接:wangafu.net/~nickm/libevent-book/Ref1_libsetup.html
Libevent 包含一些在整个进程中共享的全局设置,这些设置会影响整个库的行为。
必须在调用 Libevent 库的任何其他功能之前修改这些全局设置。若未按此操作,Libevent 可能会进入不一致的状态。
3.1 Libevent 中的日志消息
Libevent 可记录内部错误和警告信息。如果编译时启用了日志支持,还会记录调试消息。默认情况下,这些消息会写入标准错误输出(stderr)。您可以通过提供自定义的日志函数来覆盖此默认行为。
#define EVENT_LOG_DEBUG 0
#define EVENT_LOG_MSG 1
#define EVENT_LOG_WARN 2
#define EVENT_LOG_ERR 3/* Deprecated; see note at the end of this section */
#define _EVENT_LOG_DEBUG EVENT_LOG_DEBUG
#define _EVENT_LOG_MSG EVENT_LOG_MSG
#define _EVENT_LOG_WARN EVENT_LOG_WARN
#define _EVENT_LOG_ERR EVENT_LOG_ERRtypedef void (*event_log_cb)(int severity, const char *msg);void event_set_log_callback(event_log_cb cb);
要覆盖 Libevent 的日志行为,需编写与event_log_cb
签名匹配的自定义函数,并将其作为参数传递给event_set_log_callback()
。每当 Libevent 需要记录消息时,会将消息传递给您提供的函数。若要让 Libevent 恢复默认日志行为,可再次调用event_set_log_callback()
并传入NULL
参数。
注意:在用户提供的event_log_cb
回调函数中调用 Libevent 函数是不安全的!例如,如果尝试编写一个使用 bufferevents 向网络套接字发送警告消息的日志回调,可能会遇到难以诊断的奇怪 bug。未来版本的 Libevent 可能会对部分函数移除该限制。
通常情况下,调试日志默认未启用,也不会发送到日志回调函数。如果 Libevent 在构建时支持调试日志,可手动启用。
#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffuvoid event_enable_debug_logging(ev_uint32_t which);
调试日志内容冗长,且在多数场景下并非必要。调用event_enable_debug_logging(EVENT_DBG_NONE)
可恢复默认行为;调用event_enable_debug_logging(EVENT_DBG_ALL)
则会启用所有支持的调试日志。未来版本可能会支持更细粒度的选项。
这些函数声明在<event2/event.h>
头文件中。除event_enable_debug_logging()
首次出现在 Libevent 2.1.1-alpha 版本外,其他函数最早见于 Libevent 1.0c 版本。
在 Libevent 2.0.19-stable 之前,EVENT_LOG_*
宏的名称以下划线开头,例如:_EVENT_LOG_DEBUG
、_EVENT_LOG_MSG
、_EVENT_LOG_WARN
和_EVENT_LOG_ERR
。这些旧名称已弃用,仅用于与 Libevent 2.0.18-stable 及更早版本的向后兼容,未来版本可能移除。
3.2 处理致命错误
当 Libevent 检测到不可恢复的内部错误(如数据结构损坏)时,其默认行为是调用exit()
或abort()
终止当前运行的进程。这类错误几乎总是表明代码中存在漏洞 —— 可能在您的应用程序中,也可能在 Libevent 自身。
若希望应用程序更优雅地处理致命错误,可以通过提供一个回调函数来覆盖 Libevent 的默认行为,使 Libevent 在遇到致命错误时调用该函数而非直接退出。
typedef void (*event_fatal_cb)(int err);
void event_set_fatal_callback(event_fatal_cb cb);
定义一个新函数,该函数将在 Libevent 遇到致命错误时被调用。将此函数作为参数传递给event_set_fatal_callback()
。当 Libevent 后续遇到致命错误时,会调用您提供的函数。
您的回调函数不应将控制权返回给 Libevent,否则可能导致未定义行为,Libevent 可能仍会强制退出以避免崩溃。回调函数被调用后,不应再调用任何其他 Libevent 函数。
这些函数声明在<event2/event.h>
头文件中。首次出现在 Libevent 2.0.3-alpha 版本。
3.3 内存管理
默认情况下,Libevent 使用 C 标准库的内存管理函数从堆中分配内存。若需让 Libevent 使用其他内存管理器,可通过提供自定义的malloc
、realloc
和free
函数替代实现。例如,当您希望 Libevent 使用更高效率的分配器,或需要通过插桩分配器检测内存泄漏时,即可采用此方式。
void event_set_mem_functions(void *(*malloc_fn)(size_t sz),void *(*realloc_fn)(void *ptr, size_t sz),void (*free_fn)(void *ptr));
以下是一个简单示例:通过替换 Libevent 的内存分配函数来统计总分配字节数。实际场景中,若 Libevent 在多线程环境下运行,可能需要添加锁机制以避免错误。
#include <event2/event.h>
#include <sys/types.h>
#include <stdlib.h>/* This union's purpose is to be as big as the largest of all the* types it contains. */
union alignment {size_t sz;void *ptr;double dbl;
};
/* We need to make sure that everything we return is on the rightalignment to hold anything, including a double. */
#define ALIGNMENT sizeof(union alignment)/* We need to do this cast-to-char* trick on our pointers to adjustthem; doing arithmetic on a void* is not standard. */
#define OUTPTR(ptr) (((char*)ptr)+ALIGNMENT)
#define INPTR(ptr) (((char*)ptr)-ALIGNMENT)static size_t total_allocated = 0;
static void *replacement_malloc(size_t sz)
{void *chunk = malloc(sz + ALIGNMENT);if (!chunk) return chunk;total_allocated += sz;*(size_t*)chunk = sz;return OUTPTR(chunk);
}
static void *replacement_realloc(void *ptr, size_t sz)
{size_t old_size = 0;if (ptr) {ptr = INPTR(ptr);old_size = *(size_t*)ptr;}ptr = realloc(ptr, sz + ALIGNMENT);if (!ptr)return NULL;*(size_t*)ptr = sz;total_allocated = total_allocated - old_size + sz;return OUTPTR(ptr);
}
static void replacement_free(void *ptr)
{ptr = INPTR(ptr);total_allocated -= *(size_t*)ptr;free(ptr);
}
void start_counting_bytes(void)
{event_set_mem_functions(replacement_malloc,replacement_realloc,replacement_free);
}
注意事项:
- 函数替换时机:替换内存管理函数会影响 Libevent 后续所有内存分配、调整和释放操作,因此必须在调用任何其他 Libevent 函数之前完成替换。否则可能导致 Libevent 使用您的
free
函数释放由 C 标准库malloc
分配的内存,引发未知问题。 - 内存对齐要求:自定义的
malloc
和realloc
函数返回的内存块需与 C 标准库保持一致的对齐方式。 realloc
特殊情况处理:需正确处理realloc(NULL, sz)
(即视为malloc(sz)
),需正确处理realloc(ptr, 0)
(即视为free(ptr)
)。free
与malloc
的特殊情况:free
函数无需处理free(NULL)
,malloc
函数无需处理malloc(0)
。- 线程安全要求:若在多线程环境中使用 Libevent,替换的内存管理函数需具备线程安全性。
- 内存释放一致性:Libevent 会使用您替换的函数分配返回给应用的内存。因此,若需释放 Libevent 分配的内存,且已替换
malloc
/realloc
,则必须使用对应的自定义free
函数。
event_set_mem_functions()
函数声明在<event2/event.h>
头文件中,首次出现在 Libevent 2.0.1-alpha 版本。
编译时配置:Libevent 可在构建时禁用event_set_mem_functions()
。若禁用,使用该函数的程序将无法编译或链接。在 Libevent 2.0.2-alpha 及后续版本中,可通过检查EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED
宏是否定义来判断该功能是否可用。
3.4 线程间互斥
如您在编写多线程程序时可能了解的,多个线程同时访问相同数据并非始终安全。
Libevent 结构体在多线程环境中通常有三种工作模式:
- 固有单线程结构体:同一时间仅允许单个线程使用,多线程并发访问存在安全隐患。
- 可选锁定结构体:可针对每个对象指定是否需要支持多线程并发访问。
- 始终锁定结构体:若 Libevent 运行时启用锁定支持,此类结构体始终可安全地在多线程中并发使用。
若要在 Libevent 中启用锁定机制,必须先告知 Libevent 应使用的锁定函数,且此操作需在调用任何会分配需在线程间共享的结构体的 Libevent 函数之前完成。
使用 pthreads 或 Windows 原生线程:若使用 pthreads 库或 Windows 原生线程代码,可直接使用预定义函数配置 Libevent,使其自动适配对应平台的锁定机制。
#ifdef WIN32
int evthread_use_windows_threads(void);
#define EVTHREAD_USE_WINDOWS_THREADS_IMPLEMENTED
#endif
#ifdef _EVENT_HAVE_PTHREADS
int evthread_use_pthreads(void);
#define EVTHREAD_USE_PTHREADS_IMPLEMENTED
#endif
// 函数成功返回 0,失败返回 -1
若需使用其他线程库,需自行定义以下功能的实现函数:
- 锁相关:加锁(locking),解锁(unlocking),锁分配(lock allocation),锁销毁(lock destruction)。
- 条件变量相关:条件变量创建(condition variable creation),条件变量销毁(condition variable destruction),等待条件变量(waiting on a condition variable),条件变量信号 / 广播(signaling/broadcasting to a condition variable)。
- 线程相关:线程 ID 检测(thread ID detection)。
完成函数定义后,通过 evthread_set_lock_callbacks
和 evthread_set_id_callback
接口将这些函数告知 Libevent。
#define EVTHREAD_WRITE 0x04
#define EVTHREAD_READ 0x08
#define EVTHREAD_TRY 0x10#define EVTHREAD_LOCKTYPE_RECURSIVE 1
#define EVTHREAD_LOCKTYPE_READWRITE 2#define EVTHREAD_LOCK_API_VERSION 1struct evthread_lock_callbacks {int lock_api_version;unsigned supported_locktypes;void *(*alloc)(unsigned locktype);void (*free)(void *lock, unsigned locktype);int (*lock)(unsigned mode, void *lock);int (*unlock)(unsigned mode, void *lock);
};int evthread_set_lock_callbacks(const struct evthread_lock_callbacks *);void evthread_set_id_callback(unsigned long (*id_fn)(void));struct evthread_condition_callbacks {int condition_api_version;void *(*alloc_condition)(unsigned condtype);void (*free_condition)(void *cond);int (*signal_condition)(void *cond, int broadcast);int (*wait_condition)(void *cond, void *lock,const struct timeval *timeout);
};int evthread_set_condition_callbacks(const struct evthread_condition_callbacks *);
evthread_lock_callbacks
结构体用于描述锁定回调函数及其能力,具体字段要求如下:
- lock_api_version:必须设置为
EVTHREAD_LOCK_API_VERSION
。 - supported_locktypes:需设置为
EVTHREAD_LOCKTYPE_*
常量的位掩码,用于声明支持的锁类型。(截至 2.0.4-alpha 版本,EVTHREAD_LOCK_RECURSIVE
为必需项,EVTHREAD_LOCK_READWRITE
暂未使用。) - alloc:函数需返回指定类型的新锁。
- free:函数需释放指定类型锁占用的所有资源。
- lock:尝试以指定模式获取锁,成功返回 0,失败返回非零值。
- unlock:尝试释放锁,成功返回 0,失败返回非零值。
支持的锁类型:
类型常量 | 说明 |
---|---|
0 | 普通锁(不一定是递归锁)。 |
EVTHREAD_LOCKTYPE_RECURSIVE | 递归锁:允许已持有锁的线程再次获取锁,其他线程需等待该线程完全释放锁后才能获取。 |
EVTHREAD_LOCKTYPE_READWRITE | 读写锁:允许多个线程同时读,但同一时间仅允许单个线程写,写操作会排斥所有读操作。 |
支持的锁模式:
模式常量 | 说明 |
---|---|
EVTHREAD_READ | 仅用于读写锁:以读模式获取或释放锁。 |
EVTHREAD_WRITE | 仅用于读写锁:以写模式获取或释放锁。 |
EVTHREAD_TRY | 仅用于加锁操作:仅当锁可立即获取时才执行加锁,否则失败。 |
id_fn
回调函数,该函数需返回 unsigned long
类型值,用于标识调用线程。要求同一线程必须始终返回相同值,若两个线程同时执行,不得返回相同值。
evthread_condition_callbacks
结构体用于描述条件变量相关的回调函数,字段要求如下:
- lock_api_version:必须设置为
EVTHREAD_CONDITION_API_VERSION
。 - alloc_condition:函数需返回新条件变量的指针,参数为 0。
- free_condition:函数需释放条件变量占用的存储和资源。
- wait_condition:函数接收三个参数:分配的条件变量(alloc_condition)、分配的锁(evthread_lock_callbacks.alloc)、可选超时参数。函数逻辑需满足:释放锁并等待条件变量被信号通知或超时;错误返回 -1,条件满足返回 0,超时返回 1;返回前需重新获取锁。
- signal_condition:根据broadcast参数唤醒等待线程:
broadcast=false
时唤醒一个等待线程;broadcast=true
时唤醒所有等待线程。
更多细节可参考:pthreads 的 pthread_cond_*
函数文档;Windows 的 CONDITION_VARIABLE
函数文档。
源码的evthread_pthread.c
和evthread_win32.c
文件提供了示例程序,可以查看以进一步了解。
编译配置:Libevent 可禁用锁定支持,此时使用线程相关函数的程序将无法运行。
3.5 调试锁使用
为帮助调试锁的使用,Libevent 提供了可选的 “锁调试” 功能,该功能会包装其锁定调用以捕获常见的锁错误,包括:
- 解锁一个实际上并未持有的锁
- 重复锁定一个非递归锁
如果发生这些锁错误之一,Libevent 会因断言失败而退出。
void evthread_enable_lock_debugging(void);
#define evthread_enable_lock_debuging() evthread_enable_lock_debugging()
此函数必须在创建或使用任何锁之前调用。为确保安全,应在设置线程函数后立即调用它。
这个函数在 Libevent 2.0.4-alpha 版本中首次出现,当时名称拼写错误为 “evthread_enable_lock_debuging ()”。在 2.1.2-alpha 版本中,拼写被修正为 evthread_enable_lock_debugging ();目前两个名称都支持。
3.6 调试事件对象
Libevent 可检测并报告以下常见的事件使用错误:
- 将未初始化的
struct event
当作已初始化对象使用。 - 尝试重新初始化一个已处于 pending 状态的
struct event
。
注意:跟踪事件的初始化状态需要额外的内存和 CPU 资源,因此仅建议在实际调试程序时启用调试模式。
void event_enable_debug_mode(void);
调试模式启用规则:必须在创建任何 event_base
之前调用调试函数。
若程序使用 event_assign()
(而非 event_new()
)创建大量事件,启用调试模式可能导致内存耗尽。这是因为 Libevent 无法判断 event_assign()
创建的事件何时不再被使用(而 event_new()
创建的事件可通过 event_free()
明确释放)。
若需避免调试时内存耗尽,可显式告知 Libevent 某些事件不再使用:
event_debug_unassign(event *ev); // 当调试未启用时,此调用无效果
下面是示例代码:
#include <event2/event.h>
#include <event2/event_struct.h>#include <stdlib.h>void cb(evutil_socket_t fd, short what, void *ptr)
{/* We pass 'NULL' as the callback pointer for the heap allocated* event, and we pass the event itself as the callback pointer* for the stack-allocated event. */struct event *ev = ptr;if (ev)event_debug_unassign(ev);
}/* Here's a simple mainloop that waits until fd1 and fd2 are both* ready to read. */
void mainloop(evutil_socket_t fd1, evutil_socket_t fd2, int debug_mode)
{struct event_base *base;struct event event_on_stack, *event_on_heap;if (debug_mode)event_enable_debug_mode();base = event_base_new();event_on_heap = event_new(base, fd1, EV_READ, cb, NULL);event_assign(&event_on_stack, base, fd2, EV_READ, cb, &event_on_stack);event_add(event_on_heap, NULL);event_add(&event_on_stack, NULL);event_base_dispatch(base);event_free(event_on_heap);event_base_free(base);
}
详细的事件调试功能仅可在编译时通过 CFLAGS 环境变量 -DUSE_DEBUG
启用。启用后,编译的程序会输出详细日志,包含:
- 事件添加与删除记录
- 平台特定的事件通知信息
该功能无法通过 API 调用动态启用 / 禁用,仅适用于开发版本。
3.7 检查libevent版本
Libevent 的新版本会新增功能并修复漏洞。有时需要检测 Libevent 的版本,以便:
- 确认已安装的 Libevent 版本是否满足程序构建要求;
- 显示 Libevent 版本用于调试;
- 根据版本向用户提示已知漏洞或提供兼容方案。
#define LIBEVENT_VERSION_NUMBER 0x02000300
#define LIBEVENT_VERSION "2.0.3-alpha"
const char *event_get_version(void);
ev_uint32_t event_get_version_number(void);
注意:宏定义提供编译时的 Libevent 版本,函数则返回运行时版本。若程序动态链接 Libevent,这两个版本可能不同。
Libevent 版本有两种表示形式:
-
字符串格式:适合向用户展示(如 “2.1.12-stable”)。
-
4 字节整数格式:便于数值比较,字节分配规则为:
-
高字节:主版本号(Major)
-
次高字节:次版本号(Minor)
-
次低字节:补丁版本号(Patch)
-
低字节:发布状态(0 表示正式版,非 0 表示开发版)
-
正式版 Libevent 2.0.1-alpha 的版本号为 0x02000100
(二进制:02 00 01 00)。
2.0.1-alpha 到 2.0.2-alpha 之间的开发版可能为 0x02000108
(低字节非 0 表示开发状态)。
编译期检查示例:
#include <event2/event.h>#if !defined(LIBEVENT_VERSION_NUMBER) || LIBEVENT_VERSION_NUMBER < 0x02000100
#error "This version of Libevent is not supported; Get 2.0.1-alpha or later."
#endifint
make_sandwich(void)
{/* Let's suppose that Libevent 6.0.5 introduces a make-me-asandwich function. */
#if LIBEVENT_VERSION_NUMBER >= 0x06000500evutil_make_me_a_sandwich();return 0;
#elsereturn -1;
#endif
}
运行期检查示例:
#include <event2/event.h>
#include <string.h>int
check_for_old_version(void)
{const char *v = event_get_version();/* This is a dumb way to do it, but it is the only thing that worksbefore Libevent 2.0. */if (!strncmp(v, "0.", 2) ||!strncmp(v, "1.1", 3) ||!strncmp(v, "1.2", 3) ||!strncmp(v, "1.3", 3)) {printf("Your version of Libevent is very old. If you run into bugs,"" consider upgrading.\n");return -1;} else {printf("Running with Libevent version %s\n", v);return 0;}
}int
check_version_match(void)
{ev_uint32_t v_compile, v_run;v_compile = LIBEVENT_VERSION_NUMBER;v_run = event_get_version_number();if ((v_compile & 0xffff0000) != (v_run & 0xffff0000)) {printf("Running with a Libevent version (%s) very different from the ""one we were built with (%s).\n", event_get_version(),LIBEVENT_VERSION);return -1;}return 0;
}
event_get_version()
函数最早见于 Libevent 1.0c 版本;其他版本检测功能(如整数格式宏)首次出现在 Libevent 2.0.1-alpha 版本。
3.8 释放全局libevent数据
即使释放了通过 Libevent 分配的所有对象,仍会残留一些全局分配的结构体。这通常不会造成问题,因为进程退出时这些资源会自动清理。但某些调试工具可能会将这些残留结构误判为内存泄漏。若需确保 Libevent 释放所有内部全局数据结构,可调用:
void libevent_global_shutdown(void);
该函数不会释放 Libevent 返回给应用程序的任何结构体(如事件、event_base、bufferevent 等)。若需在程序退出前释放所有资源,需手动释放这些对象。调用libevent_global_shutdown()
后,其他 Libevent 函数的行为将变得不可预测。该函数应作为程序中调用的最后一个 Libevent 函数。
该函数支持重复调用(即使已调用过也不会产生副作用)。
Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~