libevent(1)之基础概述

发布于:2025-06-25 ⋅ 阅读:(21) ⋅ 点赞:(0)

Libevent(1)之基础概述


Author: Once Day Date: 2025年6月23日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

全系列文章可参考专栏: 十年代码训练_Once-Day的博客-CSDN博客

参考文章:


1. 概述
1.1 介绍

libevent是一个功能强大的跨平台事件驱动库,在高性能网络编程领域有着广泛的应用。它最初由Niels Provos等人开发,最早可以追溯到2000年。经过多年的发展演进,目前已经成为了业内公认的优秀开源项目之一。

作为一个专注于事件驱动和异步I/O的编程库,libevent帮助开发者以一种高效而统一的方式处理各种类型的事件,比如网络I/O、定时器、信号等。它能够自动利用操作系统提供的最佳I/O多路复用机制,如Linux上的epoll、BSD上的kqueue等,从而实现了可移植性和高性能。

除了跨平台和高性能之外,libevent的一大特点就是接口灵活且使用简单。它提供了基本的事件驱动编程原语,开发者可以在此基础上实现自己的应用逻辑。同时libevent也提供了一些高级组件如bufferevent,使得处理一些常见场景变得更加方便。

与libevent类似的还有libev和libuv等事件驱动库。libev较为精简,注重性能表现,但功能不如libevent丰富。而libuv则起初是为Node.js项目开发,提供了更多高层的抽象。相比之下,libevent在灵活性和跨平台支持上更胜一筹。

当然,libevent也并非完美无缺。它的接口较为底层,在实现一些复杂逻辑时还是需要写不少代码。而且在Windows平台上发挥最佳性能需要较新的系统支持。对于SSL/TLS的处理,libevent也依赖于第三方库如OpenSSL。

即便有这些限制,但瑕不掩瑜,libevent仍然是一个优秀的开源项目,在许多知名软件中都能找到它的身影。对于希望打造高性能网络应用的开发者而言,libevent无疑是一个值得掌握和运用的利器。它不仅仅是一个库,更是一种高效处理并发的编程思路和模式。

1.2 异步事件

三种:网络 io 事件、定时事件以及信号事件。

linux:epoll、poll、select,mac:kqueue,window:iocp。

定时事件:红黑树,最小堆:二叉树、四叉树,跳表,时间轮。

信号事件。

在libevent中,事件主要分为三大类:网络I/O事件、定时事件和信号事件。这三类事件分别对应了网络编程、定时任务调度和进程信号处理等常见场景。

(1)网络I/O事件是指发生在文件描述符上的可读、可写等事件。当某个文件描述符(通常是socket)上有数据可读或者可写时,就会触发相应的事件回调函数。libevent会根据不同操作系统平台,选择最优的I/O多路复用机制来监听这些事件。在Linux上,依次选择epoll、poll和select;在BSD系(如macOS)上使用kqueue;而在Windows上则使用IOCP。这些机制都能够同时监听多个文件描述符,并在事件发生时通知应用程序,从而避免了阻塞等待的情况,提高了并发性能。

(2)定时事件用于在指定的时间点触发回调函数。libevent内部使用高效的数据结构来管理定时事件,常见的有以下几种:红黑树、最小堆(二叉堆、四叉堆)、跳表和时间轮。红黑树是一种自平衡二叉搜索树,插入、删除和搜索的平均时间复杂度都是O(logN);最小堆可以在O(logN)时间内插入和删除,且能够在O(1)时间内取得最小值;跳表通过多级链表实现,其插入、删除和搜索的平均复杂度也是O(logN);时间轮则是一种环形队列结构了,通过哈希的方式将事件分散到不同的槽位中,定时操作的时间复杂度可以达到O(1)。libevent会根据定时事件的数量和分布情况,自动选择最合适的数据结构。

(3)信号事件对应进程接收到的信号,如SIGINT、SIGTERM等。libevent允许注册信号处理函数,以异步的方式响应信号。这种方式避免了信号处理函数中的阻塞操作影响主程序loop的问题。在实现上,libevent在不同系统中采取了不同的方案:在类Unix系统中,通过sigaction()注册信号处理函数,并利用socketpair()在信号发生时通知事件循环;而在Windows上,则使用Event对象来模拟信号事件。不过需要注意,在信号处理函数中要尽量避免调用非异步安全(async-signal-safe)的函数,以免引起竞态条件等问题。

1.3 helloworld示例

下面是一个使用libevent编写的简单的TCP echo服务器示例,它监听指定端口,将客户端发送的数据原样返回:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>

static void echo_read_cb(struct bufferevent *bev, void *ctx) {
    struct evbuffer *input = bufferevent_get_input(bev);
    struct evbuffer *output = bufferevent_get_output(bev);
    evbuffer_add_buffer(output, input);
}

static void echo_event_cb(struct bufferevent *bev, short events, void *ctx) {
    if (events & BEV_EVENT_ERROR)
        perror("Error from bufferevent");
    if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
        bufferevent_free(bev);
    }
}

static void accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx) {
    struct event_base *base = evconnlistener_get_base(listener);
    struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
    bufferevent_setcb(bev, echo_read_cb, NULL, echo_event_cb, NULL);
    bufferevent_enable(bev, EV_READ|EV_WRITE);
}

int main(int argc, char **argv) {
    struct event_base *base;
    struct evconnlistener *listener;
    struct sockaddr_in sin;

    int port = 9876;

    base = event_base_new();
    if (!base) {
        fprintf(stderr, "Could not initialize libevent!\n");
        return 1;
    }

    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);

    listener = evconnlistener_new_bind(base, accept_conn_cb, NULL, LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&sin, sizeof(sin));
    if (!listener) {
        fprintf(stderr, "Could not create a listener!\n");
        return 1;
    }

    event_base_dispatch(base);

    evconnlistener_free(listener);
    event_base_free(base);

    return 0;
}

这个示例程序的逻辑如下:

  1. 首先初始化libevent的event_base,它是libevent的核心组件,负责管理各种事件。
  2. 然后创建一个evconnlistener,用于监听指定端口的TCP连接请求。当有新的客户端连接到来时,会自动调用accept_conn_cb回调函数。
  3. 在accept_conn_cb函数中,为每个新的客户端连接创建一个bufferevent对象,并设置读写回调函数。当客户端发送数据时会触发echo_read_cb,当发生异常时会触发echo_event_cb。
  4. 在echo_read_cb函数中,从bufferevent的输入缓冲区读取客户端发送的数据,然后将其复制到输出缓冲区,这就实现了echo的功能。
  5. 最后在main函数中调用event_base_dispatch启动事件循环,监听并处理各种事件,直到程序退出。

这个示例虽然简单,但展示了libevent异步I/O编程的基本流程:初始化event_base、创建事件监听器、定义事件回调函数,以及启动事件循环等。可以看到,借助libevent提供的接口,编写高性能的网络应用变得简洁明了。

2. libevent原理
2.1 事件处理流程

创建事件event,添加到event_base,调用event_base_loop/event_base_dispatch进入分发,调用event_base_loopexit/event_base_loopbreak 退出事件循环,调用event_del+event_free删除事件并回收资源,调用event_base_free回收event_base资源。

对于fork的子进程,需要使用event_reinit重新初始化。

在这里插入图片描述

事件处理流程图,引用自C++网络库:Libevent网络库的原理及使用方法

2.2 事件状态

Libevent 的事件对象(struct event)主要有三个状态:非未决未决激活

包括API函数:event_new、event_set、event_add、event_del、event_free。

在这里插入图片描述

事件状态流程图,引用自C++网络库:Libevent网络库的原理及使用方法

2.3 缓冲区事件(bufferevent)

在libevent中,bufferevent是一个高级的I/O抽象,它在普通的event基础上提供了更多的功能和便利性。bufferevent内部维护了两个缓冲区(用户区)和一个文件描述符(通常是网络socket)。这两个缓冲区分别用于读写数据,起到了在用户代码和内核之间缓存数据的作用。

通过使用bufferevent,我们可以方便地读写数据,而不必直接操作底层的文件描述符。当数据可读时,内核会自动将数据读取到bufferevent的输入缓冲区;当我们往bufferevent的输出缓冲区写入数据时,内核也会自动将数据发送出去。这种机制大大简化了I/O操作,使得编程更加高效和便捷。

除了数据缓冲功能,bufferevent还支持设置三个回调函数:读回调、写回调和事件回调。这三个回调函数分别在不同的事件发生时被调用:

  • 读回调:当输入缓冲区有可读数据时触发,我们可以在这个回调函数中从输入缓冲区读取数据进行处理。
  • 写回调:当输出缓冲区的数据被发送完毕后触发,通常在这个回调函数中继续往输出缓冲区写入新的数据。
  • 事件回调:当发生一些特殊的事件时触发,如连接关闭、发生错误等,我们可以在这个回调函数中进行相应的处理。

通过设置这三个回调函数,我们就可以在不同的事件发生时执行相应的逻辑,从而实现灵活的I/O控制。

2.4 连接监听器(evconnlistener)

libevent中的连接监听器(evconnlistener)是一个用于接受传入连接的专用对象。它封装了底层socket通信的细节,使得监听和接受新连接变得简单和直观。

evconnlistener内部隐藏了socket编程中一系列繁琐的步骤,包括创建socket、绑定地址和端口(bind)、开始监听(listen)等。当我们创建一个evconnlistener时,实际上相当于一次性完成了这些底层操作。

一旦evconnlistener创建成功,它就开始监听指定的地址和端口,等待新的客户端连接到来。这里的监听过程是完全异步和非阻塞的,不会影响事件循环中其他事件的处理。

当有新的客户端连接到达时,evconnlistener会自动调用accept函数接受这个连接,并创建一个与之对应的文件描述符(fd)。接着,它会调用我们预先设置好的回调函数,将这个新的连接交给用户代码进一步处理。在回调函数中,我们可以根据需要为这个新连接创建bufferevent等对象,以便读写数据。

使用evconnlistener接受连接的优势在于,它将底层的socket操作完全封装起来,暴露给用户一个简洁的回调接口。我们只需要定义一个回调函数,在其中处理新连接即可,不必关心底层的socket细节。这种方式不仅简化了编程,也提高了代码的可读性和可维护性。

此外,evconnlistener还提供了一些有用的特性,比如设置监听队列大小、绑定多个地址、设置超时时间等。这些特性进一步增强了其灵活性和实用性。

2.5 libevent数据结构

在libevent的内部实现中,为了高效地管理各种事件,它使用了几个关键的数据结构。

libevent使用三个双向链表来分别存储不同类型的事件:

  • I/O事件链表:这个链表存储了所有注册的I/O事件(如读写事件),每个节点表示一个事件,包含事件类型、文件描述符、回调函数等信息。
  • 信号事件链表:这个链表存储了所有注册的信号事件,每个节点表示一个信号事件,包含信号值、回调函数等信息。
  • 活动事件链表:这个链表存储了所有已经就绪(激活)的事件,这些事件可能来自I/O事件、信号事件或定时器事件。libevent会依次处理这个链表中的事件。

使用双向链表的好处是插入、删除操作的时间复杂度为O(1),而且可以方便地在表头表尾操作,非常适合事件管理的场景。

其次,对于定时器事件,libevent使用了一个小根堆(最小堆)来存储。小根堆是一种常见的优先队列数据结构,它能够在O(logN)的时间内完成插入和删除操作,并且能够在O(1)时间内获取最小值(即最近的定时器事件)。

libevent在每次事件循环中,都会检查小根堆的堆顶元素,如果其超时时间已到,就将其激活,并移入到活动事件链表中等待处理。

除了这些主要的数据结构,libevent还使用了其他一些辅助的数据结构,如哈希表用于快速查找事件,数组用于保存不同优先级的事件等。

2.6 事件抽象原理

libevent的核心就是对各种事件的抽象和统一管理,使得开发者可以用一致的方式处理不同类型的事件。

对于I/O事件,libevent主要是对系统提供的I/O多路复用机制进行了封装,比如select、poll、epoll、kqueue等。这些机制允许同时监视多个文件描述符的状态变化,当其中某些文件描述符就绪时(如可读、可写),就会通知应用程序进行相应的处理。libevent将这些底层接口封装起来,提供了统一的事件注册、派发接口,简化了I/O事件的处理。

信号事件的处理则相对复杂一些。为了将信号事件集成到libevent的事件管理中,libevent创建了一个socketpair,即一对相互连接的匿名socket。其中一个socket用于监听信号事件,另一个socket用于写入信号通知。当信号发生时,信号处理函数会往写socket中写入数据,这样监听socket上就会产生可读事件,从而触发相应的事件回调。这种方法巧妙地将信号事件转化为了I/O事件,使得信号事件也能被libevent统一管理起来。

对于定时器事件,libevent利用了I/O多路复用机制的超时等待特性。像select和epoll_wait等函数都允许指定一个最长等待时间,即使没有事件发生,它们也会在超时后返回。libevent根据所有定时器事件的最小超时时间来设置系统I/O的超时值,当I/O函数返回时,再检查和激活所有已经到期的定时器事件。这样,定时器事件就巧妙地融入到了整个事件驱动框架中,与I/O事件一起被libevent统一调度。

libevent的事件抽象原理体现了软件设计中的一些重要思想,如封装、抽象、分层等。通过对底层复杂性的封装,向上提供简单统一的接口,极大地提高了开发效率和代码质量。这也是libevent能够成为广受欢迎的高性能网络库的重要原因。

2.7 多线程处理

在使用libevent进行多线程编程时,需要特别注意线程安全问题。默认情况下,libevent的API是非线程安全的,如果多个线程同时操作同一个event_base实例,就可能导致数据竞争和不可预期的行为。

一个常见的问题是多个线程同时修改event_base,例如注册新事件、删除事件等。这些操作会修改event_base内部的数据结构,如果没有适当的同步机制, 就会引发竞态条件和数据破坏。

为了在多线程环境中安全地使用libevent,一种常见的方法是为每个线程创建独立的event_base实例。每个线程只操作自己的event_base,这样就避免了线程间的相互干扰。线程可以各自管理自己的事件和任务队列,独立地进行事件派发和处理。

在Memcached中,主线程负责接受新的客户端连接,并将其分配给工作线程处理。每个工作线程都有自己独立的event_base和任务队列,负责处理分配给自己的客户端请求。这种设计避免了线程间的竞争,同时也提高了并发处理的效率。

在多线程环境中使用libevent需要仔细考虑线程安全问题,最安全和简单的方法是为每个线程使用独立的event_base实例,避免线程间的直接竞争。如果必须在线程间共享event_base,就需要使用libevent提供的线程安全API,并且小心地进行线程同步。







Alt

Once Day

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

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

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


网站公告

今日签到

点亮在社区的每一天
去签到