libev源码解析——监视器(watcher)结构和组织形式

        在《libev源码解析——总览》中,我们介绍了libev的一些重要变量在不同编译参数下的定义位置。由于这些变量在多线程下没有同步问题,所以我们将问题简化,所提到的变量都是线程内部独有的,不用考虑任何多线程问题。(转载请指明出于breaksoftware的csdn博客)

        之前提到过,libev支持多种功能,比如文件状态监控、定时器等。这些功能都是有其相对应的一个“监视器”(watcher),比如文件监视器、相对时间定时器监视器等。虽然这些监视器很多,但是它们都共有一些属性

#ifndef EV_COMMON
# define EV_COMMON void *data;
#endif

#ifndef EV_CB_DECLARE
# define EV_CB_DECLARE(type) void (*cb)(EV_P_ struct type *w, int revents);
#endif

#if EV_MINPRI == EV_MAXPRI
# define EV_DECL_PRIORITY
#elif !defined (EV_DECL_PRIORITY)
# define EV_DECL_PRIORITY int priority;
#endif

/* shared by all watchers */
#define EV_WATCHER(type)			\
  int active; /* private */			\
  int pending; /* private */			\
  EV_DECL_PRIORITY /* private */		\
  EV_COMMON /* rw */				\
  EV_CB_DECLARE (type) /* private */

        active表示这个监视器是否处于激活状态。

        priority表示监视器的优先级,其值可以从-2~2,共5个级别。其中2是最高级别,-2是最低级别。级别高的监视器会优先于级别低的监视器执行。

        cb是事件响应函数指针,data则是用于保存用户自定义的数据。这样的组合设计在使用回调函数的开源库中很常见。因为回调的调用机会并不由我们掌握,我们无法区分每次回调对应于我们哪次注册行为。而可以通过在向框架注册回调函数时保存回调调用的数据来达到区分的目的。

        pending用于表示该监视器在触发过的相同优先级下所有监视器数组的索引下标。因为相同优先级的监视器可能有很多,所以我们需要一个结构保存这样的一组数据,于是就需要索引/下标进行区分。这块信息我们将在《libev使用方法和源码解析——关键结构和基本原理2》介绍。

        最简单的监视器,也是最基础的监视器是ev_watcher。它只具有EV_WATCHER声明的变量

typedef struct ev_watcher
{
  EV_WATCHER (ev_watcher)
} ev_watcher;

        由于相同类型的监视器可能有多个,所以我们需要一个结构保存这么一组监视器。于是libev使用链表的形式保存这样的数据。那使用什么类型的链表呢?如果我们每个监视器的内存结构大小相同,则我们可以使用连续的内存结构。可是之后我们会介绍到,不同监视器的大小是不一样的。于是libev使用的是堆上分配的单向链表结构。至于实现,我们只要在结构中适当位置保存指向下一个结构地址的指针即可

#define EV_WATCHER_LIST(type)			\
  EV_WATCHER (type)				\
  struct ev_watcher_list *next; /* private */

typedef struct ev_watcher_list
{
  EV_WATCHER_LIST (ev_watcher_list)
} ev_watcher_list;

        其他监视器都是在此结构末尾追加了各自需要记录的数据。比如IO监视器和子进程监视器

typedef struct ev_io
{
  EV_WATCHER_LIST (ev_io)

  int fd;     /* ro */
  int events; /* ro */
} ev_io;

typedef struct ev_child
{
  EV_WATCHER_LIST (ev_child)

  int flags;   /* private */
  int pid;     /* ro */
  int rpid;    /* rw, holds the received pid */
  int rstatus; /* rw, holds the exit status, use the macros from sys/wait.h */
} ev_child;

        我们需要注意下这样设计的用意。从下图可见,任何监视器都可以被按ev_watcher大小准确切分,这意味着我们可以使用ev_watcher指向任何监视器结构体。同样的,还可以使用ev_watcher_list指向任何监视器。这种设计的优点将在后面展现出来。

        看了这些监视器,我们还不能察觉到libev的底层原理。现在我们回忆下之前的介绍——libev是一个基于事件的循环库。那么事件将是一个核心,然而事件需要一个文件描述符(fd)。文件描述符将和这些监视器如何协作呢?

        我们可以想象出,一个文件描述符应该关联起来多个监视器。比如我们要监视一个文件是否可读,那么这个监视器将和文件描述符关联。我们还要监视这个文件是否可写入,那么又有一个监视器和这个文件描述符关联。那么这些不同的监视器将如何围绕在这个文件描述符周围呢?链表!之前提到的ev_watcher_list链表,它可以把不同类型的监视器连接在一起。libev就是这么做的,它定义了一个结构ANFD

typedef ev_watcher_list *WL;

typedef struct
{
  WL head;
  unsigned char events; /* the events watched for */
  unsigned char reify;  /* flag set when this ANFD needs reification (EV_ANFD_REIFY, EV__IOFDSET) */
  unsigned char emask;  /* the epoll backend stores the actual kernel mask in here */
  unsigned char unused;
#if EV_USE_EPOLL
  unsigned int egen;    /* generation counter to counter epoll bugs */
#endif
#if EV_SELECT_IS_WINSOCKET || EV_USE_IOCP
  SOCKET handle;
#endif
#if EV_USE_IOCP
  OVERLAPPED or, ow;
#endif
} ANFD;

        我们只需要关注head和events变量。

        head从名字上就可以看出它是一个监视器链表的头。这儿提一句,我们看到这是一个单向链表,这也意味着以后要对这个链表进行元素新增很有可能是在头部插入,因为那样做最高效了。

        events变量表示和文件描述符关联的事件,为什么要记录这个数据呢?继续以之前的例子为例,我们先要监控这个文件的可读,于是events是EV_READ;现在我们还要监控其可写,于是events变成EV_WRITE|EV_READ,而相应的head下将有两个监视器。此时IO模型(select/poll/epoll等)将监视该文件描述符的可读可写,如果发生任何之一,将使用发生的事件去head下各个监视器去匹配。

        ANFD结构不需要再扩展了,于是它的结构是稳定的。所以我们可以使用连续的地址空间去保存一组信息。而且我们可以使用文件描述符的值去做其数组下标,这样就可以很方便通过文件描述符找到其对应的监视器链表。

        当然ANFD使用连续内存也是有个前提的,就是文件描述符的值必须在一定的值以下。为什么呢?比如文件描述符的值如果能达到0xFFFFFFFF,那么这个数组要有0xFFFFFFFF个元素?这明显是不能接受的。所幸,系统的文件描述符值的上限只有几万。         在libev中,它使用anfds保存上述数组。数组的大小也并非一开始就使用文件描述符上限值,而是随着使用的文件描述符值增大而增大。

#define array_needsize(type,base,cur,cnt,init)			\
  if (expect_false ((cnt) > (cur)))				\
    {								\
      int ecb_unused ocur_ = (cur);					\
      (base) = (type *)array_realloc				\
         (sizeof (type), (base), &(cur), (cnt));		\
      init ((base) + (ocur_), (cur) - ocur_);			\
    }

void noinline
ev_io_start (EV_P_ ev_io *w) EV_THROW
{
  int fd = w->fd;

  if (expect_false (ev_is_active (w)))
    return;

  assert (("libev: ev_io_start called with negative fd", fd >= 0));
  assert (("libev: ev_io_start called with illegal event mask", !(w->events & ~(EV__IOFDSET | EV_READ | EV_WRITE))));

  EV_FREQUENT_CHECK;

  ev_start (EV_A_ (W)w, 1);
  array_needsize (ANFD, anfds, anfdmax, fd + 1, array_init_zero);

        数组重分配后,就让文件描述符值作为下标的ANFD结构的head指向新的监视器

  wlist_add (&anfds[fd].head, (WL)w);

        理论上来说,我们有了这么一个结构就可以满足libev运行起来了。但是有个问题没法解决,那就是libev的特性——权限高的优先执行。下一节我们将就这个问题作出解释。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云计算D1net

云计算技术将如何影响商业世界?

云计算技术已经渗透到人们生活的各个方面,其中包括个人喜好和专业工作。例如,很多音乐和电视节目都可以在云端存储和访问。从待办事项列表到随机笔记,手机的所有内容都可...

18030
来自专栏Python爬虫实战

MySQL 从零开始:09 计算字段

在数据库中存储公司信息,一般用两个表列分别表示公司名和公司地址。 如果想要在一个字段中既显示公司名,又要显示公司地址,那么就需要对已有字段进行处理了,这个处理过...

10520
来自专栏互扯程序

Java开发中遇到的那些坑!

之前在这个手册刚发布的时候看过一遍,当时感觉真是每个开发者都应该必读的一本手册,最近由于在总结一些我们日常开发中容易忽略的问题,可能是最低级的编码常见问题,往往...

10510
来自专栏JAVA烂猪皮

Redis集群

一个简单粗暴的方案是部署多台一模一样的Redis服务,再用负载均衡来分摊压力以及监控服务状态。这种方案的优势在于容错简单,只要有一台存活,整个集群就仍然可用。但...

63920
来自专栏我就是马云飞

ArrayList到底是什么?

ArrayList是日常开发中使用最频繁的集合类。首先这边简单介绍一下ArrayList:

20420
来自专栏MixLab科技+设计实验室

一道关于知识的公式 -03

这是mixlab无界社区的成员Jeff的《如何让机器量化知识》系列文章的第03篇。为我们介绍知识的数据化、量化,以及如何把开放的问题转化为封闭式问题让机器解读。

12350
来自专栏FreeBuf

网上谍影:境外间谍情报机关通过互联网窃取机密

央视网消息(焦点访谈):今天是《中华人民共和国反间谍法实施细则》颁布一周年。实施《反间谍法》就是要为开展反间防谍工作、维护国家安全提供法律保障。在互联网技术高度...

14530
来自专栏有趣的Python和你

Python玩转简书钻

2018年11月15号,简书迎来大变革,取消了以往的积分制度,换为去中心化的简书钻,每日发放一万简书钻。首先,简书给出了获取钻石的途径:写文点赞,与以往的阅读,...

16020
来自专栏空木白博客

Photoshop常用快捷组合技巧工具箱快捷键

M 矩形、椭圆选框工具 C 裁剪工具 V 移动工具 L 套索、多边形套索、磁性套索 W 魔棒工具 J 喷枪工具 B 画笔工具 S 像皮图章、图案图...

12020
来自专栏Linyb极客之路

唯品会架构师是如何实现架构重构的

随着唯品会业务的快速发展,订单量的不断增长,原有的订单存储架构已经不能满足公司的发展了,特别是在大促高峰期,原订单库已经成为抢购瓶颈,已经严重制约公司的发展。

19820

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励