libev
libev
是一个高效的异步I/O库,采用了事件循环模型。用户向libev
注册感兴趣的事件,如文件描述符可读等,当事件发生时,用户注册事件时的回调被调用。
libev
支持的事件有:
- 文件描述符事件(描述符可读、可写),
ev_io
- Linux的
inotify
接口,ev_stat
- 信号事件,
ev_signal
- 定时事件,
ev_timer
- 周期事件,
ev_periodic
- 进程状态变化,
ev_child
- 事件循环自身的事件,
ev_idle
,ev_prepare
和ev_check
一个例子
我们先看一个很简单的例子,然后用这个例子的执行流程去分析libev
源码。
1 | #include <stdio.h> |
libev
中一个事件有一个watcher
表示,一个watcher
的类型的格式为ev_TYPE
。
每一个watcher
有对应的初始化函数ev_TYPE_init
,以ev_io
为例,它的初始化函数原型为:
1 | void ev_io_init(ev_io * w, void (*cb)(EV_P int revents), int fd, int events); |
注:注意的一点是,在libev
的实现中,ev_io_init
实际上是一个宏,但是我们把它理解成一个函数其实区别并不大。
ev_io_init
内部调用了两个函数:
1 | void ev_init(ev_watcher *w, void (*cb)(EV_P int revents)); |
在初始化之后就调用ev_io_start
在loop中注册事件,最后调用ev_run
运行loop。
当标准输入可读时,我们注册的回调模块stdin_watcher
会被调用,这时候我们就可以读取标准输入的数据,如果读到了EOF
,那么就调用ev_io_stop
停止这个监听事件。
源码分析
数据结构
以下的代码都是以2.0版本为准的。
在分析源码之前,我们先来看看libev
中几个关键的数据结构。
EV_WATCHER
1 | struct ev_watcher { |
ev_watcher
相当于所有watcher的父类,它含有所有watcher的通用数据。拿ev_io
来说,它的结构如下:
1 | struct ev_io { |
可以看到一个ev_io
指针可以转换成一个ev_watcher
指针,其他watcher类型也可以这么操作,所以ev_watcher
相当于所有watcher的父类。
ANFD
1 | struct ANFD { |
ANFD
表示一个文件描述符对应的事件。一个文件描述符可以有多个watcher,它们以链表的形式被组织起来,head
即是链表的头。在ev_loop
结构中,有一个anfds
数组,每一个数组元素即数组下标对应的文件描述符的ANFD
。
ANPENDING
1 | struct ANPENDING { |
一个ANPENDING
即一个待处理的事件。在ev_loop
中,待处理的事件的组织形式如下所示:
1 | pri_max |----| |----|----|----|----|----| | --|---> | | | | | | . |----| |----|----|----|----|----| . | | ANPENDINGS . |----| | | |----| | | pendings[w->priority][w->pending]即对应watcher的ANPENDING |----| | | |----| | | pri_min |----| |
ev_loop
中每一个ANPENDING
都有一个优先级,高优先级的事件在一个事件循环中首先被处理,但是低优先级事件也一定会被执行,只不过执行被延后了而已。
EV_LOOP
1 | struct ev_loop { |
ev_loop
显然是libev
中最重要的结构,这里只列出部分元素的含义,其余部分等在分析对应源码时再作解释。
ev_rt_now
:用于记录ev_loop
的现在时间。libev
中的计时器是基于真实时间的,如果你注册了一个超时事件,事件在一小时之后发生,之后你把系统时间设置成去年的某个事件,注册的事件也会在大约一小时后发生。activent
:watcher必须保持ev_loop
存活,这样每当一个事件发生时,watcher的回调函数才能被执行。为了保持ev_loop
存活,watcher必须调用ev_ref
增加activent
的个数,若activent
值为0,那么这一次事件循环之后,ev_loop
就被摧毁了。loop_count
:记录了ev_loop
事件迭代的次数backend_modify
:ev_loop
添加或修改事件监听的接口,依平台而定。libev
支持的接口有- select
- poll
- epoll
- kqueue
- port
backend_poll
:ev_loop
调用平台相关接口监听相关事件的接口。backend_fd
:以epoll
为例,其值为我们调用epoll_create
接口创建的文件句柄。
ev_io
首先来看一下ev_io
的执行流程
ev_io_start
1 | void noinline |
主要步骤为:
- 执行
ev_start
,调整watcher的优先级,设置watcher的active
标志同增加ev_loop
的activent
。 - 在文件描述符对应的watcher链表中插入该
ev_io
。 - 调用
fd_change
,它增加fdchangecnt
的个数,同时记录发生变化的文件描述符,以便在事件循环的时候处理它。
ev_loop
在最新版本中ev_loop
对应的函数为ev_run
,由于我看的是2.0版本的,就用ev_loop
来说明了。
ev_loop
1 | void |
fd_reify
1 | void inline_size |
epoll_poll
1 | static void |
call_pending
1 | void inline_speed |
主体部分就在do {} while
这个循环里面。
我们先略过不和ev_io
相关的部分,只看代码中标注([A-E])位置对应的部分:
A
:检查activent
值是否为0,若是,那么loop退出事件循环B
:调用fd_reify
函数,该函数遍历fdchanges
数组,对于每一个描述符,如果其对应的事件有改变或者新增加的描述符,那么就调用backend_modify
修改或添加文件描述符的事件。C
:增加事件循环的迭代次数,然后调用backend_poll
调用相关平台的接口监听文件描述符事件。D
:以epoll
为例,backend_poll
的实现为epoll_poll
。epoll_poll
调用epoll_wait
,发生的事件被存放在epoll_events
数组中,对于一个事件,得到的事件不是我们想要的事件,那么就修改或删除文件描述符对应的监听事件。然后调用fd_event
函数,把得到文件描述符事件加到ev_loop
的对应的pendings
列表中。在fd_event
之后,如果发现epoll_eventmax == eventcnt
,那么就增大epoll_events
数组元素的个数,以便下一次能够接收更多发生的文件描述符事件。E
:调用call_pending
函数:- 按照优先级从大到小,遍历
pendings
数组 - 如果对应的
pendingcnt[pri]
值大于0,即对应优先级有事件待处理,依次去对应ANPENDING
列表的元素 - 对取到的
ANPENDING
,用EV_CB_INVOKE
宏调用其对应watcher的回调函数。
- 按照优先级从大到小,遍历
ev_io_stop
1 | void noinline |
clear_pending
1 | void inline_speed |
ev_stop
1 | void inline_size |
A
:从pendings
列表中删除对应的watcherB
:从文件描述符对应的watcher链表anfds[w->fd]
中删除将被停止的watcher。C
:调用ev_stop
,减少ev_loop
中activent
的个数(通过ev_unref
实现),讲watcher的active
标志置为0。D
:调用fd_change
函数修改对应的fdchanges
数组和fdchangecnt
变量,以便在下一次事件循环中修改文件描述符的监听事件。如果文件描述符没有任何监听事件,那么在文件描述符的epoll事件会在epoll_poll
函数中被删除。
1 | if (expect_false (got & ~want)) |
ev_timer & ev_periodic
ev_timer
和ev_periodic
都可以用来设置超时和周期事件,不同的是,ev_periodic
可以设置一个回调函数,在每一次周期完成后这个回调函数被调用并返回一个时间节点,该节点是下一次事件被触发的时间。
ev_loop
结构内有两个元素:
- timers
- periodics
timer
和periodic
的结构分别为:
timer
1 | struct ev_timer { |
periodic
1 | struct ev_periodic { |
它们分别存储了在ev_loop
中注册的所有ev_timer
和ev_periodic
。它们都以最小堆的形式被组织,堆顶是离现在最近的timer事件。每一次事件循环,在调用backend_poll
之前,首先取两个堆顶的元素,取时间较小的那个作为此次backend_poll
的超时事件。在backend_poll
返回之后调用timers_reify
和periodics_reify
调整堆,同时把已经发生的超时和定时事件加入到pendings
中。
1 | if (expect_false (flags & EVLOOP_NONBLOCK || idleall || !activecnt)) |
time_update
:主要用于更新ev_loop
的当前时间。backend_fudge
:时间误差变量
time_reify
和periodic_reify
函数如下:
1 | void inline_size |
以periodics_reify
例子(timers_reify
类似):
- 如果堆顶的时间比现在的事件小,那么取堆顶,否则函数返回
- 如果是周期事件,即
reschedule_cb
不为空或者interval
不为0,那么计算出下一次事件触发的事件,调整堆,否则停止这个timer事件。 - 把这一次触发的事件加入到
pendings
中,等待call_pending
被调用而触发回调函数。
ev_signal
libev
加入了对信号事件的支持。当一个信号发生时,回调函数不会像在UNIX系统中一样被立刻调用,而是在下一个事件循环中被处理。
先看看信号事件在libev
中的组织形式。
1 | |--------| |----|----|----|----|----| | head |---> | | | | | | . | | |----|----|----|----|----| . | gotsig | EV_WATCHERS . |--------| | | | | | | |--------| | | | | | | |--------| |
相关的数据结构:
1 | struct ANSIG { |
当某一个信号被触发时,信号对应的WL
中所有的回调函数都会被依次执行。
libev
利用管道实现了异步信号处理。一个loop在调用loop_init
初始化之后,调用siginit
:
1 | static void noinline |
可以看siginit
往loop中注册一个ev_io
,用于监听管道中pipe[0]
的读事件。
用户在注册一个信号事件时,调用ev_signal_init
设置信号的回调处理,监听的信号值等。然后调用ev_signal_start
:
1 | ... |
首先讲watcher加入对应信号的链表,然后如果是链头,那么说明对应的信号处理函数未被注册到内核中,于是初始化一个sigaction
,注册对应的信号处理函数。sighandler
代码如下:
1 | static void |
可以看到,当一个信号发生时,libev
设置信号的对应gosig
为1,然后往管道离写信号值,这样先前注册的读管道监视器sigev
就被激活,其对应的回调函数被在下一个事件循环被调用:
1 | static void |
在这之后所有的信号事件被加入pendings
中,当call_pending
被调用时,信号事件的回调事件也就得到了处理。
ev_prepare & ev_check
ev_prepare
和ev_check
是ev_loop
事件循环自身的事件。ev_prepare
在ev_loop
收集事件前被调用;ev_check
在收集完事件后被调用。他们都能唤醒和休眠任意个监视器,以实现一些特定的事件循环行为。
ev_prepare
和ev_check
的结构为:
1 | struct ev_TYPE { |
ev_stat
ev_stat
相关接口没有看,因为对inotify
和kqueue
接口还不是很熟悉(逃