您的位置:首页 > 其它

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

2017-06-22 14:33 387 查看
请注意这是 libev 而不是 libevent 的文章!

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

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


ev_io:直接操作fd

这个 watcher 负责检测文件描述符(以下简称fd)是否可写入数据或者是读出数据。最好是将fd设置为非阻塞的。

  注意有时候在调用
read
时是没有数据的(返回0),此时一个一个非阻塞的
read
会得到
EAGAIN
错误。

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


失踪的 fd 的特殊问题

部分系统需要显式地调用
close
(如
kqueue
epoll
),否则当一个
fd 消失、而新的 fd 进入,占用同一个 fd 号时,
libev
不知道这是一个新的fd。

  libev 一侧解决的办法是每次调用
ev_io_set
时,都假定这是一个新的 fd。


使用
dup
操作 fd 的特殊问题

一些后端(backend)不能注册普通的 fd 事件,只能注册
underlying file descriptions
,这意味着使用
dup()
或其他奇怪操作的fd,只能由其中一个被接收到。

  这没有有效的解决办法,除非将后端设置为
BACKEND_SELECT
EVBACKEND_POLL


关于文件的特殊问题

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


关于 fork 的特殊问题

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


关于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_WRITE
EV_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_suspend
ev_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)


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

绝对计时器:offset 等于绝对时间,interval 为0,reschedule_cb 为 NULL。在这种设置下,时钟只执行一次,不重复

重复内部时钟:offset 小于等于 interval 值,interval 大于0,reschedule_cb 为 NULL。这种设置下,watcher 永远在每一个(offset + N * interval)超时。

手动排程模式: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_statdata
struct
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 响应服务器的简单流程
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: