Libev 官方文档学习笔记 - 03:常用 watcher 接口

amc

请注意这是 libev 而不是 libevent 的文章!

这篇文章是第三篇,主要讲 libev 里基本集中的 watcher。

本文地址:https://segmentfault.com/a/1190000006679929


ev_io:直接操作fd

这个 watcher 负责检测文件描述符(以下简称fd)是否可写入数据或者是读出数据。最好是将fd设置为非阻塞的。
  注意有时候在调用read时是没有数据的(返回0),此时一个一个非阻塞的read会得到EAGAIN错误。

(以下两个特殊问题,是 libev 文档中特别提到的,但是我看不太懂……)

失踪的 fd 的特殊问题

部分系统需要显式地调用close(如kqueueepoll),否则当一个 fd 消失、而新的 fd 进入,占用同一个 fd 号时,libev不知道这是一个新的fd。
  libev 一侧解决的办法是每次调用ev_io_set时,都假定这是一个新的 fd。

使用dup操作 fd 的特殊问题

一些后端(backend)不能注册普通的 fd 事件,只能注册underlying file descriptions,这意味着使用dup()或其他奇怪操作的fd,只能由其中一个被接收到。
  这没有有效的解决办法,除非将后端设置为BACKEND_SELECTEVBACKEND_POLL

关于文件的特殊问题

ev_io对于文件泪说没有什么用,只要文件存在,就立即会有时间。对于stdinstdout,请谨慎使用,确保这两者没有被重定向至文件。

关于 fork 的特殊问题

记得使用ev_loop_fork,并且使用EVFLAG_FORKCHECK。不过对于epollkqueue之外的无需担心。

关于SIGPIPE的问题

只是提醒一下:记得处理SIGPIPE事件。

关于accept一个无法接受的连接

大多数 POSIX accpet 实现中在删除因为错误而导致的连接时(如 fd 到达上限)都回产生一个错误的操作,比如使 accept 失败但不拒绝连接,只产生ENFILE错误。但这个会导致 libev 还是将其标记为 ready 状态。
  推荐方法是列出所有的错误并记录下来,或者是暂时关闭 watchers。

相关函数

void ev_io_init (ev_io *, callback, int fd, int events)
void ev_io_set (ev)io *, int fd, int events)

其中 events 可以是EV_WRITEEV_READ的组合。

示例

static void stdin_readable_db (struct ev_loop *loop,
                               ev_io *w,
                               int revents)
{
    ev_io_stop (loop, w)
    ......    // 从 w->fd 中进行read
}

......

some_init_func ()
{
    ......
    struct ev_loop *loop = ev_default_init (0);
    ev_io stdin_readable;
    ev_io_init (&stdin_readable, stdin_readable_db , STDIN_FILENO, EV_READ);
    ev_io_start (loop, &stdin_readable);
    ev_run (loop, 0);
    ...
}

ev_timer:相对超时机制

Libev 提供了一个相对超时机制的定时器。所谓的“相对”,就是说这个定时器的参数是:指定以当前时间为基准,延迟多久出发事件。这个定时器与基于万年历的日期/时间是无关的,只基于系统单调时间。

循环定时器设计

下面列出一个以60秒为单位的循环定时器作为例子,来说明使用ev_timer的不同策略

1. 使用标准的初始化和停止 API 来重设

ev_timer_init (timer, callback, 60.0, 6.0);
ev_timer_start (loop, timer)

标准设置。或——

ev_timer_stop (loop, timer);
ev_timer_set (timer, 60.0, 0.0);
ev_timer_start (loop, timer)

这样的设置,当每次有活跃时间时,停止timer,并且重启它。第一个参数是首次超时,第二个参数是第二次开始的固定超时时间。
  但是这样的方法虽然比较简易,但是时间不稳定,而且开销较大

2. 使用ev_timer_again重设

使用ev_timer_again,可以忽略ev_timer_start

ev_init (timer, callback);
timer->repeat = 60.0;
ev_timer_again (loop, start);

上面的初始化完成后,在 callback 里调用:

timer->repeat = 60.0;
ev_timer_again (loop, timer);

可以改变 timeout 值,不管 timer 是否 active

3. 让 timer 超时,但视情况重新配置

这个方式的基本思路是因为许多 timeout 时间都比 interval 大很多,此时要记住上一次活跃的时间,然后再 callback 中检查真正的 timeout

ev_tstamp g_timeout = 60.0;
ev_tstamp g_last_activity;
ev_timer  g_timer;

static void callback (EV_P_ev_timer *w, int revents)
{
    ev_tstamp after = g_last_activity - ev_now(EV_A) + g_timeout;
    
    // 如果小于零,表示时间已经发生了,已超时
    if (after < 0.0) {
        ......    // 执行 timeout 操作
    }
    else {
        // callback 被调用了,但是却有一些最近的活跃操作,说明未超时
        // 此时就按照需要设置的新超时事件来处理
        ev_timer_set (w, after, 0.0);
        ev_timer_start (loop, g_timer);
    }
}

启用这种模式,记得初始化时将g_last_activity设置为ev_now,并且调用一次callback (loop, &g_timer, 0);当活跃时间到来时,只需修改全局的 timeout 变量即可,然后再调用一次 callback

g_timeout = new_value
ev_timer_stop (loop, &timer)
callback (loop, &g_timer, 0)

4. 为 timer 使用双向链表

使用场景:有成千上万个请求,并且都需要 timeouts
  当 timeout 开始前,计算 timeout 的值,并且将 timeout 放在链表末尾。然后当链表前面的项需要 fire 时。使用ev_timer来将其 fire 掉。
  当有 activity 时,将 timer 从 list 中一处,重算 timeout,并且再附到 list 末尾,确保如果ev_timer已经被 list 的第一项取出时,更新它

“太早”的问题

假设在500.9秒的时候请求延时1秒,那么当501秒到来时,可能导致 timeout,这就是“太早”问题。Libev的策略是对于这种情况,在502秒时才执行 timeout。但是这又有“太晚”的问题,请程序员注意.

“假死”问题

Suspenged animation,也称为休眠,指的是将机子置于休眠状态。注意不同的机子不同的系统这个行为可能不一样。
  其中一种休眠是使得所有程序感觉只是经过了很小的一段时间一般(时间跳跃)
  推荐在SIGTSTP处理中调用ev_suspendev_resume

其他注意点

ev_now_update()的开销很大,请谨慎使用
  Libev使用的时一个内部的单调时钟而不是系统时钟,而ev_timer则是基于系统时钟的,所以在做比较的时候两者不同步。

相关函数

void ev_timer_init (ev_timer *, callback, ev_tstamp after, ev_tstamp repeat);
void ev_timer_set (ev_timer *, ev_tstamp after, ev_tstamp repeat);

如果repeat为正,这个timer会重复触发,否则只触发一次。

void ev_timer_again (loop, ev_timer *)
ev_tstamp ev_timer_remaining (loop, ev_timer *)

ev_periodic:基于日历的定时器

相关函数

void ev_periodic_init (ev_periodic *, callback, ev_tstamp offset, 
                       ev_tstamp interval, reschedule_cb)
void ev_periodic_set (ev_periodic *, ev_tstamp offset,
                      ev_tstamp interval, reschedule_cb)

以下是几种不同应用场景的设置方法:

  1. 绝对计时器:offset 等于绝对时间,interval 为0,reschedule_cb 为 NULL。在这种设置下,时钟只执行一次,不重复
  2. 重复内部时钟:offset 小于等于 interval 值,interval 大于0,reschedule_cb 为 NULL。这种设置下,watcher 永远在每一个(offset + N * interval)超时。
  3. 手动排程模式:offset 忽略,reschedule_cb 设置。使用 callback 来返回下次的 trigger 时间。callback 原型为:
ev_tstamp (*reschedule_cb)(ev_periodic *w, ev_tstamp now);

例程是:

static ev_tstamp my_scheduler (...)
{
    return now + 60.0;
}

类似于 Linux 内核的jiffies,返回下一个时间点。

这个timer非常便于用来提供诸如“下一个正午12点”之类的定时器。

void ev_periodic_again (loop, ev_periodic *)

关闭并重启 watcher,参见前文。

ev_tstamp ev_periodic_at (ev_periodic *)

返回下一次触发的绝对时间。

ev_signal:捕获 signal 事件

在哦你跟一个 loop 可以多次观测同一个 signal,但是无法在多个 loop 中观测同一个 signal。此外,SIGCHILD只能在 default loop 中监听。

注意点

关于继承 fork / execve / ptherad_create 的问题

在子进程调用 exec 之前,应当将 signal mask 重设为你所需的默认值。最简单的方法就是子进程做一个pthread_atfork()来重设。

关于线程信号处理

POSIX 的不少功能(如sigwait)只有在进程中的所有线程屏蔽了 signal 时才真正生效
  为了解决这个问题,如果真的要使用这些功能的话,建议在创建线程之前屏蔽所有的 signal,并且在创建 loops 的时候指定EVFLAG_NOSIGMASK,然后制定一个 thread 用来接收 signals。

相关函数

void _ev_signal_init (ev_signal *, callback, int signum)
void ev_signal_set (ev_signal *, int signum)

ev_child:子进程退出事件

当接收到SIGCHILD事件时,child watcher 触发。大部分情况下,子进程退出或被杀掉。只要这个 watcher 的 loop 未开始,你甚至可以在 shild 被 fork 之后才加入 child watcher。
  Ev_child 的优先级固定是EV_MAXPRI

void ev_chile_init (ev_child *, callback, int pid, int trace)
void ev_child_set (ev_child *, int pid, int trace)

Pid 如果指定0的话,表示任意子进程。可以在 ev_child 中观察rstatus成员来了解子进程状态。

int pid;

表示监控中的 pid,只读。

int rpid;

可读写,表示检测到状态变化的 pid

int tstatus;

可读写,表示由 rpid 导致的进程的 exit/trace 状态值。

ev_stat:监控文件属性变化

使用 ev_stat 时,监控目标位置上无需存在文件,因为文件从“不存在”变为存在也是一种状态变化。
  文件路径必须是绝对路径,不能存在“./”或“../”。
  Ev_stat 的实现其实只是定期调用stat()来判断文件属性的变化,所以可以指定检查周期。指定0的话会使用默认事件周期。
  正因为这是轮询操作,所以这个功能不适合做大数据量或者是大并发检测;同时,ev_stat 是异步的。

大文件支持

默认关闭大文件支持(使用32位的stat)。如果要使用大文件支持(ABI),libev 的作者在这里吐槽,说你要游说操作系统的发布方去支持……囧rz

关于文件时间

有些系统的文件时间仅精确到秒,这就意味着 ev_stat 无法区分秒以下的变动。

相关函数和数据成员

void ev_stat_init (ev_stat *, callback, const char *path, ev_tstamp interval);
void ev_stat_set (ev_stat *, const char *path, ev_tstamp interval);
void ev_stat_stat (loop, ev_stat *);

第三个函数使用新的文件 stat 值去更新 stat buffer,使用此函数来使得你做的一些配置更改不会被触发。

ev_statdata attr

只读,代表文件最近一次的状态。ev_statdatastruct stat基本是相通的。

ev_statdata prev

文件上一次的状态

ev_tstamp interval
const char *path

都是只读,字面意义上的意思。

ev_idle:无事可做时的事件

void ev_idle_init (ev_idle *, callback)

这个功能没有研究过,暂记着把。

其他事件(仅记录)

ev_prepare 和 ev_check

ev_embed

ev_fork

ev_cleanup

ev_asunc

其他函数

void ev_once (loop, int fd, int events, ev_tstamp timeout, callback)

从指定的f fd 中指定一个超时事件,这个函数的方便之处在于无需做 alloc/conf/start/stop/free
  Fd 可以小于0,这样就没有 I/O 监控,并且“events”会被忽略。

void ev_feed_event (loop, int fd, int revents);

向一个 fd 发送事件。需要注意的是,这个功能貌似是只能在 loop 内调用才有效,异步地在 loop 的另一个线程直接调用是无效的。

void ev_feed_signal_event (loop, signum)

向一个 loop 模拟 signal。参见 ev_feed_signal

系列篇

Libev 官方文档学习笔记(1)——概述和 ev_loop
Libev 官方文档学习笔记(2)——watcher 基础
Libev 官方文档学习笔记(3)——常用 watcher 接口(本文)
使用 libev 构建 TCP 响应服务器的简单流程

阅读 9.7k

后台 / 嵌入式全栈之路
曾经的嵌入式 / 后台开发一枚,现在开始走向架构。本专栏没有高深技术,只讲基础组件、工具,请放心食用

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发

798 声望
164 粉丝
0 条评论
你知道吗?

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发

798 声望
164 粉丝
宣传栏