libevent介绍(二)——事件循环使用入门

2,945 阅读6分钟

一、创建和释放event_base

创建event_base结构体有两种方式,第一种方式是简单的使用默认设置的event_base:struct event_base *event_base_new(void)。第二种方式较为复杂,需要先创建event_config配置结构体,再通过结构体创建event_base:

struct event_config *event_config_new(void); // 分配一个新的event_config
struct event_base *event_base_new_with_config(const struct event_config *cfg); // 获得新的event_base
void event_config_free(struct event_config *cfg); // 释放掉event_config

释放event_base的方式很简单,调用void event_base_free(struct event_base *base)即可,释放event_base不会释放任何处理了的时间,也不会关闭socket或释放指针。

1.1 配置event_base

对于复杂的event_base创建方式而言,创建event_config结构体后可以通过如下几个设置来配置。

  1. 函数int event_config_avoid_method(struct event_config *cfg, const char *method)可以配置不要选择哪些后端。例:event_config_avoid_method(cfg, "select"),可以选择的后端有:select、poll、epoll、kqueue、devpoll、evport、win32。
  2. 函数int event_config_require_features(struct event_config *cfg, enum event_method_feature feature)可以告诉libevent不要使用无法提供其中某个功能的后端。其中event_method_feature结构的定义和含义如下:
enum event_method_feature {
    EV_FEATURE_ET = 0x01, // 需要支持边缘触发的后端
    EV_FEATURE_O1 = 0x02, // 需要添加或删除或激活某个事件的操作开销为O(1)
    EV_FEATURE_FDS = 0x04, // 需要支持任意文件描述符类型,而不仅仅时套接字的后端
};
  1. 函数int event_config_set_flag(struct event_config *cfg, enum event_base_config_flag flag)可以告诉libevent在构建event_base时,要基于event_base_config_flag运行标志:
enum event_base_config_flag {
    EVENT_BASE_FLAG_NOLOCK = 0x01, // 不要为event_base分配锁,可能减少事件开销,但会导致多线程下的不安全
    EVENT_BASE_FLAG_IGNORE_ENV = 0x02, // 不要检查EVENT_*环境变量,可能debug困难
    EVENT_BASE_FLAG_STARTUP_IOCP = 0x04, // 仅在windows下有效,会使libevent启用任何必要的IOCP调度逻辑
    EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08, // 无需在每次事件循环准备运行超时回调时检查事件,在每次超时回调后检查时间,可能导致更多的CPU开销
    EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10, // 如果使用epoll后端,可以使用更快的基于"changelist"的后端,epoll-changelist后端可以避免不必要的系统调用
    EVENT_BASE_FLAG_PRECISE_TIMER = 0x20 // 尝试使用系统最快的可用计时机制,如果有更细粒度的计时精度,那么使用该机制
};

1.2 重新初始化event_base

并非所有事件后端在调用fork()后都能保持纯净,因此,如果程序使用fork()或相关系统调用来启动新进程后,如果希望继续使用event_base,则可能需要重新初始化:

int event_reinit(struct event_base *base);

二、事件注册

libevent的基础操作就是事件,事件代表的是一组条件:

  • 一个准备好用于读或写的事件描述符
  • 一个将要可以用于读或写的事件描述符
  • 一个超时
  • 一个信号通知
  • 一个用户触发的事件 事件的生命周期类似,调用libevent以设置事件,并将事件与event_base关联后,事件会被初始化。在此时,可以添加事件使得事件被挂起,当事件处于挂起状态时,如果发生触发事件的条件,将激活事件使之进入活动状态,并运行其用户提供的回调函数。如果事件是永久的,则保持挂起状态,否则当回调运行时,将停止挂起。可以通过删除挂起事件使挂起事件变成非挂起的,也能通过添加非挂起的事件使之变得挂起。

2.1 构建事件对象

使用event_new接口创造对象。定义如下:

#define EV_TIMEOUT      0x01
#define EV_READ         0x02
#define EV_WRITE        0x04
#define EV_SIGNAL       0x08
#define EV_PERSIST      0x10
#define EV_ET           0x20

typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

struct event *event_new(struct event_base *base, evutil_socket_t fd,
    short what /* 定义如上 */, event_callback_fn cb,
    void *arg); // 新建对象

void event_free(struct event *event); // 释放对象

如果fd是非负的,那么fd就是需要观察的用于读或写的事件。当事件被激活,libevent会调用cb函数,并传递文件描述符、触发的所有事件以及传递给函数的arg值。
所有的新事件都是初始化并且未挂起的,为了挂起事件,可以调用event_add()函数。

2.1.1 事件配置

  1. EV_TIMEOUT
    这个标志指示事件超时后变为活动状态。

  2. EV_READ 设置读取的事件标识。

  3. EV_WRITE 设置写入的事件标识。

  4. EV_SIGNAL 基于信号的事件。

  5. EV_PERSIST 默认情况下,当一个挂起的事件被激活,会在callback函数被执行完前进入非挂起状态。因此如果想要使事件再次进入挂起状态,可以在callback内再次调用event_add()。如果设置了EV_PERSIST标志,则事件是永久的,意味着事件会一直保持挂起状态。

  6. EV_ET 设置边缘触发的事件标识,会影响EV_READEV_WRITE

三、使用事件循环

3.1 启动循环

从一和二中,当我们拥有一个event_base,且注册了一些事件,我们需要libevent等待事件到达,并在到达时提示我们。我们可以通过int event_base_loop(struct event_base *base, int flags)函数运行event_base,直到没有更多的事件。event_base_loop函数的逻辑如下所示:

while (any events are registered with the loop,
        or EVLOOP_NO_EXIT_ON_EMPTY was set) {

    if (EVLOOP_NONBLOCK was set, or any events are already active)
        If any registered events have triggered, mark them active.
    else
        Wait until at least one event has triggered, and mark it active.

    for (p = 0; p < n_priorities; ++p) {
       if (any event with priority of p is active) {
          Run all active events with priority of p.
          break; /* Do not run any events of a less important priority */
       }
    }

    if (EVLOOP_ONCE was set or EVLOOP_NONBLOCK was set)
       break;
}

在循环中,函数会不断检查任何注册的事件是否被触发,如果触发了,则将事件设置为活动状态,并开始运行。
如果设置了EVLOOP_ONCE,则会运行活动事件并返回;如果设置了EVLOOP_NONBLOCK,则循环不会等待事件触发,仅仅会检查任何事件是否准备好了并立即运行它们。
当循环中没有任何挂起或活动状态的事件,会立即退出。设置EVLOOP_NO_EXIT_ON_EMPTY标识可以使得循环不退出。

3.2 停止循环

libevent内定义了两个函数,可以用于停止事件循环:

int event_base_loopexit(struct event_base *base,
                        const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);

区别如下:

  • event_base_loopexit()函数会告诉event_base停止,直到给定时间到达,如果tv参数为空,则event_base的事件循环会立即停止,如果正在执行活动事件的回调函数,则会继续运行直到都运行结束。如果没有事件循环在执行,loopexit会使得事件循环的下一个实例在运行下一轮回调后立即停止。
  • event_base_loopbreak()函数会立即停止循环,如果有事件正在执行,则会执行完该事件后立即结束并退出。如果没有运行中的之间循环,loopbreak不会有任何影响。