Lazy loaded image
考研
Linux 网络编程学习笔记——十二、高性能 I_O 框架库 Libevent
字数 10619阅读时长≈ 27 分钟
2026-10-16
2026-10-16

目录

在处理 I/O 事件、信号和定时事件时,需要考虑如下三个问题:
  • 统一事件源:很明显,统一处理这三类事件既能使代码简单易懂,又能避免一些潜在的逻辑错误。
  • 可移植性:不同的操作系统具有不同的 I/O 复用方式,比如 Solaris 的 dev/poll 文件,FreeBSD 的 kqueue 机制,Linux 的 epoll 系列系统调用。
  • 对并发编程的支持:在多进程和多线程环境下,我们需要考虑各执行实体如何协同处理客户连接、信号和定时器,以避免竞态条件。

一、I/O 框架库概述

I/O 框架库以库函数的形式,封装了较为底层的系统调用,给应用程序提供了一组更便于使用的接口。这些库函数往往比程序员自己实现的同样功能的函数更合理、更高效,且更健壮。因为它们经受住了真实网络环境下的高压测试,以及时间的考验。各种 I/O 框架库的实现原理基本相似,要么以 Reactor 模式实现,要么以 Proactor 模式实现,要么同时以这两种模式实现。举例来说,基于 Reactor 模式的 I/O 框架库包含如下几个组件:句柄(Handle)、事件多路分发器(EventDemultiplexer)、事件处理器(EventHandler)和具体的事件处理器(ConcreteEventHandler)、Reactor 。这些组件的关系下:

1. 句柄

I/O 框架库要处理的对象,即 I/O 事件、信号和定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是:当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。在 Linux 环境下,I/O 事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值。

2. 事件多路分发器

事件的到来是随机的、异步的。我们无法预知程序何时收到一个客户连接请求,又亦或收到一个暂停信号。所以程序需要循环地等待并处理事件,这就是事件循环。在事件循环中,等待事件一般使用 I/O 复用技术来实现。I/O 框架库一般将系统支持的各种 I/O 复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的 demultiplex 方法是等待事件的核心函数,其内部调用的是 select 、poll 、epoll_wait 等函数。
1
1
此外,事件多路分发器还需要实现 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 方法来删除事件多路分发器中的一个事件。

二、Libevent 源码分析

Libevent 是开源社区的一款高性能的 I/O 框架库,其学习者和使用者众多。使用 Libevent 的著名案例有:高性能的分布式内存对象缓存软件 memcached ,Google 浏览器 Chromium 的 Linux 版本。作为一个 I/O 框架库,Libevent 具有如下特点:
2
2
  • 跨平台支持:Libevent 支持 Linux 、UNIX 和 Windows ;
  • 统一事件源:Libevent 对 I/O 事件、信号和定时事件提供统一的处理;
  • 线程安全:Libevent 使用 libevent_pthreads 库来提供线程安全支持;
  • 基于 Reactor 模式的实现。

1. hello world 实例

由代码可知,调用 Libevent 库的主要逻辑如下:
  1. 调用 event_init 函数创建 event_base 对象。一个 event_base 相当于一个 Reactor 实例;
  1. 创建具体的事件处理器,并设置它们所从属的 Reactor 实例。evsignal_new 和 evtimer_new 分别用于创建信号事件处理器和定时事件处理器,它们是定义在 include/event2/event.h 文件中的宏:
    1. 可见,它们的统一入口是 event_new 函数,即用于创建通用事件处理器(图 12 - 1 中的 EventHandler)的函数。其定义是:
      • base:指定新创建的事件处理器从属的 Reactor ;
      • fd:指定与该事件处理器关联的句柄:
        • 创建 I/O 事件处理器时,应该给 fd 参数传递文件描述符值;
        • 创建信号事件处理器时,应该给 fd 参数传递信号值;
        • 创建定时事件处理器时,应该给 fd 参数传递 -1 ;
      • events:指定事件类型,其可选值都定义在 include/event2/event.h 文件中:
        • 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 事件队列、信号事件队列;
        • 事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理器中的回调函数。
可见,它们的统一入口是 event_new 函数,即用于创建通用事件处理器(图 12 - 1 中的 EventHandler)的函数。其定义是:
  • base:指定新创建的事件处理器从属的 Reactor ;
  • fd:指定与该事件处理器关联的句柄:
    • 创建 I/O 事件处理器时,应该给 fd 参数传递文件描述符值;
    • 创建信号事件处理器时,应该给 fd 参数传递信号值;
    • 创建定时事件处理器时,应该给 fd 参数传递 -1 ;
  • 创建 I/O 事件处理器时,应该给 fd 参数传递文件描述符值;
  • 创建信号事件处理器时,应该给 fd 参数传递信号值;
  • 创建定时事件处理器时,应该给 fd 参数传递 -1 ;
  • events:指定事件类型,其可选值都定义在 include/event2/event.h 文件中:
    • EV_PERSIST:事件被触发后,自动重新对这个 event 调用 event_add 函数;
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 事件队列、信号事件队列;
    • 事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理器中的回调函数。
  • 事件指的是一个句柄上绑定的事件,比如文件描述符 0 上的可读事件;
  • 事件处理器,也就是 event 结构体类型的对象,除了包含事件必须具备的两个要素(句柄和事件类型)外,还有很多其他成员,比如回调函数;
  • 事件由事件多路分发器管理,事件处理器则由事件队列管理。事件队列包括多种,比如 event_base 中的注册事件队列、活动事件队列和通用定时器队列,以及 evmap 中的 I/O 事件队列、信号事件队列;
  • 事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理器中的回调函数。
  1. 调用 event_add 函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应的事件添加到事件多路分发器中。event_add 函数相当于 Reactor 中的 register_handler 方法。
  1. 调用 event_base_dispatch 函数来执行事件循环。
  1. 事件循环结束后,使用 *_free 系列函数来释放系统资源。

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 文件:分别封装了如下 I/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 结构体,并实现了这两个结构体的相关操作。

3. event 结构体

前文提到,Libevent 中的事件处理器是 event 结构类型。event 结构体封装了句柄、事件类型、回调函数,以及其他必要的标志和数据。该结构体在 include/event2/event_struct.h 文件中定义:
  • ev_events:代表事件类型。其取值可以是 Libevent 支持的事件类型的标志的按位或(互斥的事件类型除外,比如读写事件和信号事件就不能同时被设置);
  • ev_next:所有已经注册的事件处理器(包括 I/O 事件处理器和信号事件处理器)通过该成员串联成一个尾队列,我们称之为注册事件队列。宏 TAILQ_ENTRY 是尾队列中的节点类型,它定义在 compat/sys/queue.h 文件中:
    • ev_active_next:所有被激活的事件处理器通过该成员串联成一个尾队列,我们称之为活动事件队列。活动事件队列不止一个,不同优先级的事件处理器被激活后将被插入不同的活动事件队列中。在事件循环中,Reactor 将按优先级从高到低遍历所有活动事件队列,并依次处理其中的事件处理器;
    • ev_timeout_pos:这是一个联合体,它仅用于定时事件处理器。为了讨论的方便,后面称定时事件处理器为定时器。老版本的 Libevent 中,定时器都是由时间堆来管理的。但开发者认为有时候使用简单的链表来管理定时器将具有更高的效率。因此,新版本的 Libevent 就引入了所谓“通用定时器”的概念。这些定时器不是存储在时间堆中,而是存储在尾队列中,我们称之为通用定时器队列。对于通用定时器而言,ev_timeout_pos 联合体的 ev_next_with_common_timeout 成员指出了该定时器在通用定时器队列中的位置。对于其他定时器而言,ev_timeout_pos 联合体的 min_heap_idx 成员指出了该定时器在时间 堆中的位置;
    • _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:对于 I/O 事件处理器,它是文件描述符值;对于信号事件处理器,它是信号值;
    • ev_base:该事件处理器从属的 event_base 实例;
    • ev_res:记录当前激活事件的类型;
    • ev_flags:是一些事件标志。其可选值定义在 include/event2/event_struct.h 文件中:
      • ev_pri:指定事件处理器优先级,值越小则优先级越高;
      • ev_closure:指定 event_base 执行事件处理器的回调函数时的行为。其可选值定义于 event-internal.h 文件中:
        • ev_timeout:仅对定时器有效,指定定时器的超时值;
        • ev_callback:是事件处理器的回调函数,由 event_base 调用。回调函数被调用时,它的 3 个参数分别被传入事件处理器的如下 3 个成员:ev_fd 、ev_res 和 ev_arg ;
        • ev_arg:回调函数的参数。

        4. 往注册事件队列中添加事件处理器

        前面提到,创建一个 event 对象的函数是 event_new(及其变体),它在 event.c 文件中实现。该函数的实现相当简单,主要是给 event 对象分配内存并初始化它的部分成员。event 对象创建好之后,应用程序需要调用 event_add 函数将其添加到注册事件队列中,并将对应的事件注册到事件多路分发器上。event_add 函数在 event.c 文件中实现,主要是调用另外一个内部函数 event_add_internal :
        该函数内部调用了几个重要的函数:
        • evmap_io_add:该函数将 I/O 事件添加到事件多路分发器中,并将对应的事件处理器添加到 I/O 事件队列中,同时建立 I/O 事件和 I/O 事件处理器之间的映射关系;
        • evmap_signal_add:该函数将信号事件添加到事件多路分发器中,并将对应的事件处理器添加到信号事件队列中,同时建立信号事件和信号事件处理器之间的映射关系;
        • event_queue_insert:该函数将事件处理器添加到各种事件队列中:将 I/O 事件处理器和信号事件处理器插入注册事件队列;将定时器插入通用定时器队列或时间堆;将被激活的事件处理器添加到活动事件队列中,其实现如下:

          5. 往事件多路分发器中注册事件

          event_queue_insert 函数所做的仅仅是将一个事件处理器加入 event_base 的某个事件队列中。对于新添加的 I/O 事件处理器和信号事件处理器,我们还需要让事件多路分发器来监听其对应的事件,同时建立文件描述符、信号值与事件处理器之间的映射关系。这就要通过调用 evmap_io_add 和 evmap_signal_add 两个函数来完成。这两个函数相当于事件多路分发器中的 register_event 方法,它们由 evmap.c 文件实现,其中用到了一些重要数据结构:
          由于 evmap_io_add 和 evmap_signal_add 两个函数的逻辑基本相同,因此仅讨论 evmap_io_add 函数:

          6. eventop 结构体

          eventop 结构体封装了 I/O 复用机制必要的一些操作,比如注册事件、等待事件等。它为 event_base 支持的所有后端 I/O 复用机制提供了一个统一的接口。该结构体定义在 event-internal.h 文件中:
          前文提到,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 文件中定义:
          Libevent 通过遍历 eventops 数组来选择其后端 I/O 复用技术。遍历的顺序是从数组的第一个元素开始,到最后一个元素结束。所以,在 Linux下 ,Libevent 默认选择的后端 I/O 复用技术是 epoll 。 ## 7. event_base 结构体 结构体 event_base 是Libevent的Reactor 。它定义在 event-internal.h 文件中:

          8. 事件循环

          最后,讨论一下 Libevent 的“动力”,即事件循环。Libevent 中实现事件循环的函数是 event_base_loop 。该函数首先调用 I/O 事件多路分发器的事件监听函数,以等待事件;当有事件发生时,就依次处理之:
           
          原创存证 · 本地哈希 · 06cae0ff597d...
          算法:SHA-256
          时间:2026-10-16
          哈希:06cae0ff597dedb0c824860bdfd2518788cbf6562b4ae05bd7b50455b04f2a39

          该信息用于辅助证明当前内容版本,不等同于法律公证或版权登记。


          评论
          Loading...