Libev 源码分析 - ev_io

概述

Libev 是使用 Reactor 模型的实现的一个高性能事件循环库。
它的主要实现包括:

  • 在结构上分离了事件处理逻辑和业务逻辑。
  • 抽象出一套通用的多路复用接口,使得基于 Libev 编写的程序可以在不同的多路复用接口中切换(比如 Mac 上使用 kqueue,Linux 上使用 epoll),实现跨平台运行。
  • 抽象 io/timer/signal 不同类型的事件,实现统一处理。

libev 的事件处理过程可以想象成如下的伪代码:

do_some_init()
while True:
    t = caculate_loop_time()
    deal_loop(t)
    deal_with_pending_event()
do_some_clear()

首先做一些初始化操作,然后进入到循环中。
在循环中,首先计算出 waittime,然后调用 select/poll/epoll 等多路复用接口监听 fd。
如果发现有 fd 可用,就执行对应的回调函数。

主要数据类型:

  • EV_WATCHER

    /* shared by all watchers */
    #define EV_WATCHER(type)            \\
      int active;    /* 表示 watcher 是否活跃,active = 1 表示还没被 stop 掉 */ \\
      int pending;   /* 存储 watcher 在 pendings 中的索引。大于零表示还没被处理。
                      * watcher 的回调函数被调用后,会设置为 0。 */ \\
      int priority;  /* 事件的优先级 */ \\
      void *data;    /* 回调函数所需要的数据 */ \\
      void (*cb)(EV_P_ struct type *w, int revents);  /* 回调函数 */
    作用:不同事件类型的共有信息。
    
  • EV_WATCHER_LIST

    #define EV_WATCHER_LIST(type)           \\
      EV_WATCHER (type)             \\
      struct ev_watcher_list *next;  /* 同一个文件描述符上可以被注册多个 watcher,比如:监听是否可读/可写 */
    作用:watcher 链表
    
  • ev_io

    typedef struct ev_io
    {
      EV_WATCHER_LIST (ev_io)
    
      int fd;
      int events;
    } ev_io;
    作用是:记录 IO 事件的基本信息。
    ev_io 相比 ev_watcher 增加了 next, fd, events 的属性。
    
  • ANFD

    /* file descriptor info structure */
    typedef struct
    {
      WL head;              /* 同一个 fd 上的所有 ev_watcher 事件 */
      unsigned char events; /* the events watched for,通常被设置成所有 ev_watcher->events 的或集。 */
      unsigned char reify;  /* flag set when this ANFD needs reification (EV_ANFD_REIFY, EV__IOFDSET)
                             * 默认值为 0,当调用 ev_io_start 后,reify 会被设置为 `w->events & EV__IOFDSET | EV_ANFD_REIFY`。
                             * 如果 reify 未被设置,则把 fd 添加到 fdchanges 中去。*/
      ...
    } ANFD;
    
    作用:
    在管理 io 事件的时候,如何根据 fd 快速找到与其相关的事件,是一个需要考虑的问题。
    Libev 的方法是用 anfds 数组来存所有 fd 信息的结构体,然后以 fd 值为索引直接找到对应的结构体。
    
  • ANPENDING

    /* stores the pending event set for a given watcher */
    typedef struct
    {
      W w;
      int events; /* the pending event set for the given watcher */
    } ANPENDING;
    作用:存储已准备好的 watcher,等待回调函数被调用。
    
  • ev_loop

    struct ev_loop {
      double ev_rt_now; /* 当前的时间戳 */
    
      int backend; /* 采用哪种多路复用方式, e.g. SELECT/POLL/EPOLL */
      int activecnt; /* total number of active events ("refcount") */
      int loop_done; /* 事件循环结束的标志,signal by ev_break */
    
      int backend_fd; /* e.g. epoll fd, created by epoll_create*/
      void (*backend_modify)(EV_P_ int fd, int oev, int nev)); /* 对应 epoll_ctl */
      void (*backend_poll)(EV_P_ ev_tstamp timeout)); /* 对应 epoll_wait */
    
      void (*invoke_cb)(struct ev_loop *loop);
    
      ANFD *anfds; /* 把初始化后的 ev_io 结构体绑定在 anfds[fd].head 事件链表上,方便根据 fd 直接查找。*/
    
      int *fdchanges; /* 存放需要 epoll 监听的 fd */
      ANPENDING *pendings [NUMPRI]; /* 存放等待被调用 callback 的 watcher */
    }
    作用:基本包含了 loop 循环所需的所有信息,为让注释更容易理解采用 epoll 进行说明。
    

详细可以参考:ev_vars.h 和 ev_wrap.h 文件。


## 执行流程:

- ev_io_init()
初始化 watcher 的 fd/events/callback。

- ev_io_start()
![ev_io_start 流程图](http://upload-images.jianshu.io/upload_images/21025-70a63ec369dc294f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

- ev_run()
![ev_run 流程图](http://upload-images.jianshu.io/upload_images/21025-440889db351176af.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

## 几个关键数组的增/删/修改

### loop->anfds
> 在管理 io 事件的时候,如何根据 fd 快速找到与其相关的事件,是一个需要考虑的问题。Libev 的方法是用 anfds 数组来存所有 fd 信息的结构体,然后以 fd 值为索引直接找到对应的结构体。

这样以 fd 为索引,anfds[fd] 中记录 fd 的相关信息。
- 增:在 `ev_io_start()` 中,添加 watcher 到 `anfds[fd].head` 链表。
- 删:在 `ev_io_stop()` 中,删除 `anfds[fd].head` 链表中的 watcher。
- 修改:
  - anfds[fd]->active:在 `ev_io_start() -> fd_change()` 中被修改 。
  - anfds[fd]->events:在 `ev_run() -> fd_reify()` 中被修改 ,修改为所有 watcher->events 的或集。(每循环一次就需要更新一次,因为可能有新增/删除 watcher)

### loop->fdchanges
由 fd 组成的一维数组,存储需要交给 epoll 监听的 fd。
- 增:在`ev_io_start -> fd_change` 中,如果发现 `anfds[fd]->active` �不为 0,即需要监听 fd 的�某些事件,则添加 fd 到 fdchanges 中,fdchangescnt 加 1。
- 删:在 `ev_run -> fd_reify` 中,遍历 fdchanges 数组,对 fd 执行 `epoll_modify()`。遍历完 fdchanges 数组后,fdchangescnt 被设置为 0。

### loop->pendings
存储待处理的 watcher,这是一个二维数组,第一个维度的索引是 priority,表示事件的优先级(普通用法不需要关注),第二个维度的索引被记录在 watcher->pending,方便定位。
- 增:在 `ev_run -> epoll_poll -> fd_event` 中,发现有可用的 fd,则把对应的 watcher 添加到 pendings 数组,等待执行回调函数,pendingcnt 加 1。
- 删:在 `ev_run -> ev_invoke_pending` 中,遍历 pendings,执行 watcher 上的回调函数,然后 pendingcnt 减 1。

## Q & A:

#### Libev 是如何实现 Reactor 模型的?
需要先了解 Reactor 的 [基本结构](https://en.wikipedia.org/wiki/Reactor_pattern#Structure)。
- Resources(资源): 对应 `ev_io` 结构,存放事件的基本信息。
- Synchronous Event Demultiplexer(同步事件�多路分用器): 对应 `ev_run` 的实现,所有的资源都被 block 在一个大的循环中,然后�通过多路复用监听所有的 fd,发现有可用的 fd 就把对应的事件存放在 pendings 中待处理。
- Dispatcher(调度器):对应 `ev_invoke_pending` 的实现,从 pendings 取出所有待处理的事件,执行对应的回调函数。
- Request Handler(事件处理函数): 对应被注册到 watcher �上的 callback。

#### 应用程序会 block 在 ev_run 吗?如果是,事件的 callback 什么时候执行?
在外部看来,程序会 block 在 `ev_run`。在 libev 内部,ev_run 其实是在一个大的循环中,不断取出可用的 fd,并调用对应的 callback。

#### 当某个 callback 执行时间较长时候,是否会影响到其他 callback 的执行?
`ev_invoke_pending` 从 pendings 数组中取出待执行的 watcher,并执行对应的回调函数,是同步处理的过程。如果某一个 callback 执行时间很长,会�影响到其他程序。所以 callback 中尽量少执行 IO 操作。

## 参考资料:
- [Libev 事件库源码阅读笔记](http://c4fun.cn/blog/2014/03/06/libev-study/)
- [libev ev_io 源码分析](http://csrd.aliapp.com/?p=1604)
- [libev 设计分析](https://cnodejs.org/topic/4f16442ccae1f4aa270010a3)
- [Create TCP Echo Server using Libev](http://codefundas.blogspot.com/2010/09/create-tcp-echo-server-using-libev.html)
- [Reactor pattern](https://en.wikipedia.org/wiki/Reactor_pattern)
- [高性能网络编程6--reactor反应堆与定时器管理](http://blog.csdn.net/russell_tao/article/details/17452997)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 名称 libev - 一个 C 编写的功能全面的高性能事件循环。 概要 示例程序 关于 libev Libev 是...
    hanpfei阅读 15,152评论 0 5
  • ❲追根究底❳Libevent内部实现原理初探 Libevent确实方便了开发人员,对于定时器、信号处理、关心的文件...
    meng_philip123阅读 4,559评论 0 4
  • 观察者类型 This section describes each watcher in detail, but ...
    hanpfei阅读 1,044评论 0 1
  • epoll概述 epoll是linux中IO多路复用的一种机制,I/O多路复用就是通过一种机制,一个进程可以监视多...
    发仔很忙阅读 10,858评论 4 35
  • 岁月不老, ——却惜时 耐心地数着你心跳, 决不容它多跳一秒。 你的心跳, 剩之多少? 岁月易老, ——...
    丨言笑阅读 259评论 2 2