在《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的特性——权限高的优先执行。下一节我们将就这个问题作出解释。