基本介绍

ev_io是这对I/O观察者, 通过注册组合事件来监视状态变更.

1. 文件描述符的”读”与”写”

I/O观察者会在每次事件循环迭代中检查注册的文件描述符是否可读、可写. 准确来说:

  • 每次报告事件时, 要么内核缓冲区有可被读取的数据, 要么文件描述符状态变更(关闭、出错)等.

  • 每次报告事件时, 要么内核缓冲区空闲可被写入数据, 要么文件描述符状态变更(连接建立、失败)等.

如果期间对事件后不做任何处理(关闭、停止), 那么观察者每次都会重复的报告事件. 这种行为我们称之为”level-triggering(水平触发)”.

通常我们打开/创建的文件描述符的默认是阻塞(block), 这意味着我们每次读取数据时都会将进程陷入到内核态. 所以将所有文件描述符都设置为非阻塞模式一般来说是一个好主意.

但, 当您的文件描述符无法在非阻塞模式下运行, 那么您必须自己解决如下一些问题:

  • 用额外的事件驱动接口保证事件的准确性.

  • 增加定时器、SIGALRM信号等来确保进程不会永久阻塞.

这通常会需要使用者做更多复杂的操作, 所以说如非必要请置为非阻塞(non-blocking).

最后, 当您不想再处理一个文件描述符的I/O的事件时请主动停止观察者.

2. 文件描述符”消失”

通常我们在使用一些”后端“(epollkqueuelinux aio)的时候, 在注册感兴趣的文件描述符后会主动调用例如dup2close等函数, 这些函数会直接影响文件描述符在这些事件接口里的状态.

导致它们可能会有一些出乎意料的行为, 如: 默默丢弃已注册的事件, 让内部发生异常状态. 这时候如果Libev也无法有效分辨这个文件描述符的真实性与有效性.

注意: 为了避免这种类似的情况出现, 每次调用这些方法之前最好先停止事件观察者.

3. 文件(File)的事件

许多人希望将文件的fd(使用open打开)注册到事件接口中, 借此希望磁盘访问的读、写也是这样. 然而这样的想法无法如”预期”那般到达, 只要是已打开的FILE文件, 那么每次注册后都会立刻获得“读”“写”事件.

由于高级通知机制通常不太支持文件(FILE), 所以您应该如上述那般无法使用它. 但是Libev会尽可能的模拟POSIX的相关行为, 因为这样做对下面这些行为能提供方便:

  1. 关注控制台的 stdinstdout.

  2. ttypipe/sockpairunix-domain-socket.

  3. 其它特殊设备.

    总结来说: 即使文件(FILE)因为使用异步I/O来提供服务, 但是当它仍然”能行”的时候选择它也不错.

4. SIGPIPE的特殊问题

人们似乎都很容易忘记它: 当写入已关闭的管道后您的程序会收到一个SIGPIPE, 默认情况下它会中止您的程序. 这在编码、调试期间是非常明智的行为, 但是对于后端守护进程来说这是灾难性行为.

所以. 当您无法解释程序为什么会悄无声息退出时, 请注意注册信号并忽略SIGPIPE(或记录进程退出状态, 这会在您事后排查得到很大线索).

5. accept失败的问题

许多POSIX实现的accept函数都不会从队列中删除异常的连接. 例如: 大型服务器经常因为文件描述符用完而导致接受失败(ENFILE), 但是, Libev还是会在下次事件迭代的时候发出事件. 如果您因此没有做好处理, 则可能在无法排查到问题的情况下发现CPU飙升到100%.

在每次启动服务之前最好将open files调整到一个合理的值, 可以有效的避免此类事情发生.

相关函数

ev_io_init (ev_io *, callback, int fd, int events)

ev_io_set (ev_io *, int fd, int events)

配置一个ev_io观察者, 参数fd则是文件描述符. events则是EV_READEV_WRITEEV_READ | EV_WRITE.

ev_io_modify (ev_io *, int events)

类似于ev_io_set, 但是这函数仅更改events事件. (在某些支持的后端下这个操作可能会更快)

使用这个函数Libev可以假设fd可以引用一个相同的底层文件描述符. 使用ev_io_set时则无法做到.

ev_io_start (struct ev_loop , ev_io )

在前面初始化完毕后, 调用这个方法会将ev_io注册到内部并启动.

使用示例

注册一个来自控制台的ev_io输入事件, 并将输其输出返回到控制台. 直到使用ctrl + c退出:

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
28
29
30
31
32
#include <ev.h>
#include <stdio.h>
#include <unistd.h>

void stdin_cb(struct ev_loop *loop, ev_io *w, int revents) {

char buffer[4096];
memset(buffer, 4096, 4096);
read(w->fd, buffer, 4096);
puts(buffer);
printf("等待输入: \n");
}

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

ev_io stdin_watcher;

// 在启动一个I/O观察者之前, 我们需要先初始化它.
ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);
// 启动后意味着观察者将在`stdin`变为可读后触发.
ev_io_start (loop, &stdin_watcher);
printf("等待输入: \n");

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

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