当前位置: 首页 > news >正文

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 使用其他内存管理器,可通过提供自定义的mallocreallocfree函数替代实现。例如,当您希望 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);
}

注意事项

  1. 函数替换时机:替换内存管理函数会影响 Libevent 后续所有内存分配、调整和释放操作,因此必须在调用任何其他 Libevent 函数之前完成替换。否则可能导致 Libevent 使用您的free函数释放由 C 标准库malloc分配的内存,引发未知问题。
  2. 内存对齐要求:自定义的mallocrealloc函数返回的内存块需与 C 标准库保持一致的对齐方式。
  3. realloc特殊情况处理:需正确处理realloc(NULL, sz)(即视为malloc(sz)),需正确处理realloc(ptr, 0)(即视为free(ptr))。
  4. freemalloc的特殊情况free函数无需处理free(NULL)malloc函数无需处理malloc(0)
  5. 线程安全要求:若在多线程环境中使用 Libevent,替换的内存管理函数需具备线程安全性。
  6. 内存释放一致性: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 结构体在多线程环境中通常有三种工作模式:

  1. 固有单线程结构体:同一时间仅允许单个线程使用,多线程并发访问存在安全隐患。
  2. 可选锁定结构体:可针对每个对象指定是否需要支持多线程并发访问。
  3. 始终锁定结构体:若 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_callbacksevthread_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.cevthread_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 可检测并报告以下常见的事件使用错误:

  1. 将未初始化的 struct event 当作已初始化对象使用。
  2. 尝试重新初始化一个已处于 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 的版本,以便:

  1. 确认已安装的 Libevent 版本是否满足程序构建要求;
  2. 显示 Libevent 版本用于调试;
  3. 根据版本向用户提示已知漏洞或提供兼容方案。
#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 函数。

该函数支持重复调用(即使已调用过也不会产生副作用)。







Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!

(。◕‿◕。)感谢您的阅读与支持~~~

相关文章:

  • C++11 异步编程(3)--- packaged_task
  • nginx反向代理的bug
  • 用Flink打造实时数仓:生产环境中的“坑”与“解药”
  • 备战全国青少年信息素养大赛图形化编程复赛/省赛——绘制图形
  • [数论](a % MOD + b % MOD) % MOD = (a + b) % MOD
  • 《P1637 三元上升子序列》
  • #华为昇腾#华为计算#昇腾开发者计划2025#
  • Redis学习笔记——黑马点评 附近商铺到UV统计 完结
  • 中州养老:学会设计数据库表
  • 银行账户管理系统01
  • 图解Git中Rebase与Merge的区别
  • Linux中《动/静态库原理》
  • WireShark网络取证分析第一集到第五集和dvwa靶场环境分析漏洞
  • C++并发编程-5.C++ 线程安全的单例模式演变
  • 暑假复习篇之五子棋①
  • MongoDB06 - MongoDB 地理空间
  • Cursor 教程:用 Cursor 创建第一个 Java 项目
  • Blood-Cat 公網網路攝像機泄露收集器:查看指定國家地區攝像
  • 左神算法之螺旋打印
  • Docker 镜像构建 - Aliyun