基本介绍

Libev的事件循环对象由struct ev_loop定义.

相关函数

1. struct ev_loop *ev_default_loop (unsigned int flags)

此函数将返回”默认“的ev_loop对象并初始化, 如果您不知道使用哪个事件循环, 请使用这个函数返回的ev_loop对象(或通过EV_DEFAULT宏).

如果ev_loop已经被初始化, 那么再(多)次调用都只会简单返回同一个对象(即使flags不同). 如果尚未初始化, 那么将会根据flags创建它.

注意: 此函数不是线程安全的. 所以, 要在多线程使用的时候必须加上互斥锁保证操作的原子性(虽然这种情况极少). 同时”缺省“的ev_loop对象才可以处理ev_child.

示例:

1
2
if (!ev_default_loop (0))
fatal ("could not initialise libev, bad $LIBEV_FLAGS in environment?");

限制Libev仅使用selectpoll后端同时忽略环境配置的示例:

1
ev_default_loop (EVBACKEND_POLL | EVBACKEND_SELECT | EVFLAG_NOENV);

flags参数的描述会在ev_loop_new中会详细说明.

2. struct ev_loop *ev_loop_new (unsigned int flags)

此函数创建并初始化一个新的事件循环对象.如果不能初始化循环, 则返回NULL. 函数的调用是线程安全的. 通常我们会为每一个线程创建一个ev_loop, 带在主线程中使用”缺省“的ev_loop.

flags参数可以用来指定要使用的特殊行为或特定后端, 而通常情况下可以被指定为0(或EVFLAG_AUTO宏);

以下是flags支持的标志位:

  • EVFLAG_AUTO

    默认标志位. 如果您不知道如何选择, 那么最好选择使用它.

  • EVFLAG_NOENV

    默认情况下, Libev会在环境中寻找该环境变量并且覆盖其它特殊设置. 如果指定了此标志位, Libev则不会再使用LIBEV_FLAGS环境变量. 这个标志配置对于开发期间的性能测试、Bug检查等配置尤为有用.

  • EVFLAG_FORKCHECK

    通过设置此标志位让Libev在每次事件迭代中检查fork; 通常使用的是getpid来进行检查, 这可能会因为内核(系统)的不同对迭代速度有些许影响. 优点则是无需再为fork检查担心.

    注意: 此标志不能被LIBEV_FLAGS重写或指定.

  • EVFLAG_NOINOTIFY

    当指定了此标志位, ev_stat不再尝试使用inotify来进行检查. 启用inotify则可以让ev_stat保存inotify的句柄(handle), 这通常能减少内部消耗.

  • EVFLAG_SIGNALFD

    当指定了此标志位, Libev将使用signalfdAPI来优化ev_signal实现信号处理. 这能串行化处理信号数据, 简化线程间的信号处理. 默认情况下signalfd不会被使用, 因为这会改变你的信号掩码.

  • EVFLAG_NOSIGMASK

    当指定了此标志位, Libev将避免修改信号掩码. 这意味着当你想接收信号时它们不会被阻塞.

    当您希望自己处理信号或希望在特定的线程中处理信号, 它将变得非常有用.

  • EVFLAG_NOTIMERFD

    当指定了此标志位, Libev将不会使用timerfd来检查时间跳跃. 虽然Libev仍能检查时间跳跃, 但是这会需要花费更多的时间.
    当前会在第一个周期定时器创建的时候开始使用timerfd, 如果因为各种原因失败, 则会退回到其它方法中完成.

  • EVBACKEND_SELECT (value 1, portable select backend)

    使用标准的select(2)后端, 但是Libev会尝试自己调整fd_set以达到避免fds数量限制. 如果失败, 那么使用select后端对fd的监控数量会非常低且它非常低效(O(highest_fd)). 不过, 在监视少量文件描述符事件的后端中它通常是最快的.

    此后端将EV_READ映射到readfds结合上, 将EV_WRITE映射到writefds集合.

  • EVBACKEND_POLL (value 2, poll backend)

    使用标准的poll(2)后端, 它的复杂度比select更高, 但是能解决fd_set的稀疏数组与fds的文件描述符数量限制. 不过它在拥有大量不活跃fd的时候事件通知效率毅然很低O(total_fds).

    此后端将EV_READ映射为POLLIN | POLLERR | POLLHUP, 将EV_WRITE映射为POLLOUT | POLLERR | POLLHUP.

  • EVBACKEND_EPOLL (value 4, Linux)

    使用特定于Linuxepoll(7)接口(适用于2.6之后的内核). 对于很少的fdsselectpoll稍微慢一点, 但它的扩展性则会更好. 相较于前者的O(total_fds), epoll则是O(active_fds).

    而指的一提的是作为高级事件驱动接口存在错误的设计. 所幸, 这些都可以被内部额外的编程解决. (译者: 这里相当大一部分是作者对设计的吐槽, 有兴趣的同学自己阅读原文这里不再赘译.)

    此后端映射EV_READEV_WRITE的方式与EVBACKEND_POLL相同.

  • EVBACKEND_LINUXAIO (value 64, Linux)

    4.18之后的内核中可以使用特定的Linux AIO(不是aio而是io_submit)事件接口(但Libev只会在4.19中启动它). 如果这个后端可用, 那么可能值得使用它. 否则, 最好忽略回退选择使用epoll较好.

    Linux AIO似乎并不是一个通用的后端, 所以epoll会作为协助处理无法正常工作的文件描述符. 甚至在出现内核故障的时候, 直接退回到epoll.(译者: 这里是意译.)

    此后端映射EV_READEV_WRITE的方式与EVBACKEND_POLL相同.

  • EVBACKEND_KQUEUE (value 8, most BSD clones)

    kqueue其实特别值得一提, 因为在除NetBSD之外的其它BSD实现上都有问题(Break). 然而与epoll设计不同的是, 它的这些错误可以在不更改现有API的情况下被修复. 所以, 除非您明确指定EVBACKEND_KQUEUE否则不会被作为这些平台的首选.

    kqueueepoll可扩展性一样, 但是内核的接口更为高效(并不只是说速度). 并且此后端通常在大多数情况下都表现良好.

    此后端将EV_READ映射到带有NOTE_EOFEVFILT_READ kevent,并将EV_WRITE映射到带有 NOTE_EOFEVFILT_WRITE kevent.

  • EVBACKEND_DEVPOLL (value 16, Solaris 8)

    尚未实现.

  • EVBACKEND_PORT (value 32, Solaris 10)

    Solaris 10的事件接口. 虽然它也很慢, 但是仍然能保证O(active_fds)的效率. 它在每次循环迭代中,每个活动文件描述符都需要一个系统调用. 所以在少量fds中选择使用selectpoll通常会更好.

    值得一提的是: 他们(Sun)给出的代码示例都是错的, 但是所幸的是Libev能够解决这些白痴问题(work around these idiocies)

    此后端映射EV_READEV_WRITE的方式与EVBACKEND_POLL相同.

如果上述一个或多个后端标志被添加到标志值中,那么只有这些后端会被尝试(以相反的顺序). 如果没有指定, 那将尝试 ev_recommended_backends()中的所有后端。

这个示例尝试创建一个仅使用epoll的事件循环:

1
2
3
struct ev_loop *epoller = ev_loop_new (EVBACKEND_EPOLL | EVFLAG_NOENV);
if (!epoller)
fatal ("no epoll found here, maybe it hides under your chair");

这个示例假设但如果可用, 请确保使用kqueue:

1
struct ev_loop *loop = ev_loop_new (ev_recommended_backends () | EVBACKEND_KQUEUE);

同样的, 如果可以则希望使用Linux AIO. 否则, 使用其它后端:

1
struct ev_loop *loop = ev_loop_new (ev_recommended_backends () | EVBACKEND_LINUXAIO);

3. int ev_is_default_loop (loop)

如果是”缺省“的loop返回true, 否则返回false.

4. unsigned int ev_iteration (loop)

返回当前loop的迭代次数.

5. ev_set_userdata (loop, void data) || void ev_userdata (loop)

设置与获取loop的用户自定义对象(void* data), 这通常用来让loop携带一些特殊的对象(上下文).

6. ev_verify (loop)

这个函数根据EV_VERIFY宏在内部做一些健壮性、可靠性的检查与验证, 如果发现错误会立即抛出错误消息并调用abort().

这通常在开发、调试期间尤为有用, 有利于协助我们排查问题. 而在生产环境中最好避免使用, 过多的检查会影响整体性能.

7. ev_break (loop, how)

可以用来调用ev_run提前返回(但必须在处理完所有未处理的事件之后).

  • howEVBREAK_ONE会返回一层ev_run嵌套.

  • howEVBREAK_ALL会返回所有ev_run嵌套.

ev_run返回之后再次调用ev_run则会清除break state.

ev_run(外部)调用ev_break不会产生任何影响.

8. unsigned int ev_backend (loop)

返回正在使用的后端标志位.

9. ev_run (loop, int flags)

此函数通常在初始化完成所有的观察者并且想开始处理事件之后被调用. 它将向操作系统询问任何新事件<->调用观察者回调, 然后无限期地重复这个过程.

如果flags参数为0,它将在内部持续处理事件,直到不再有事件处于活动状态或主动调用ev_break. 如果没有更多活动的观察者, 那么此函数将会返回. 请注意, 显示调用ev_break来停止事件循环通常是最好的方式.

  • EVRUN_ONCE

    查找并处理任何已触发(pending)但未完成的事件. 在至少处理了1个事件后ev_run将会直接返回.

  • EVRUN_NOWAIT

    查找并检查所有事件, 在经过一次迭代后如果没有已触发(pending)但未完成的事件则ev_run返回.

    下面则是ev_run内部大致的运行流程(不保证将来不会改变):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- 递增`loop`深度.
- 重置`ev_break`状态.
- 在第1次迭代之前,呼叫所有`pending`中的观察者。
开始循环(`LOOP`):
- 如果定义了`EVFLAG_FORKCHECK`宏, 则每次都检查`fork`.
- 如果检测到了`fork`, 则根据队列调用所有`fork watcher`.
- 根据队列调用所有`prepare watcher`.
- 如果调用了`ev_break`, 则直接结束事件循环.
- 如果`fork`已经被调用, 分离并且重新创建内核状态避免进程干扰.
- 更新未修改的内核状态.
- 更新`event loop time` (`ev_now()`).
- 如果有必要, 计算休眠和阻塞时间(`EVRUN_NOWAIT`或者没有活跃的观察者则不会导致`sleep`).
- 如果指定了I/O休眠时间, 则这里会执行.
- 递增循环迭代计数器.
- 阻塞进程等待事件来来临.
- 根据队列调用所有活跃`I/O`事件
- 更新`event loop time` (`ev_now()`) 避免时间跳跃.
- 根据队列处理超时定时器(`ev_timer`).
- 根据队列处理周期定时器(`ev_periodic`).
- 处理具有高优先级的`idle`事件.
- 根据队列处理所有(`ev_check`).
- `signal`、`async`、`child` 被作为`I/O`观察者实现并串行化执行.
- 如果调用`ev_break`或`flags`为`EVRUN_ONCE`、`EVRUN_NOWAIT`或没有活跃事件, 则结束事件循环. 否则重复上述步奏.
结束(`FINISH`):
- 根据情况重置`EV_BREAK`的状态.
- 递减`loop`深度
- `ev_run`返回.

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 只需导入单个头文件
#include <ev.h>

int main (void)
{
// 可以使用已定义的宏来获取默认的事件循环, 当然你也可以根据自己的需求创建指定的.
struct ev_loop *loop = EV_DEFAULT;

// 开始运行事件循环
ev_run (loop, 0);

// 如果事件循环退出, 那将会执行到这里.
return 0;
}