libuv源码分析(二)事件循环(Eventloop)
事件循环是 libuv 功能的核心部分。它的主要职责是对 I/O 进行轮询然后基于不同的事件源调用它们的回调。
事件循环主体数据结构在 libuv 中用 struct uv_loop_s 或类型别名 uv_loop_t 表示,文中统一使用 loop 表示其实例,它代表了事件循环,实际上它是事件循环所有资源的统一入口,所有在事件循环上运行的各类 Handle/Request 实例都被注册到 uv_loop_s 内部的各类结构中如队列、堆、伸展树等,同一 Handle 实例通常也会被关联到多个不同的结构中,如大多数 Handle 都会同时存在两个队列中。
uv_loop_t 是一种特殊的 Handle,它管理了同一事件循环上的所有资源。
uv_loop_t 实例通常需要经历 Init、Run、Stop、Close 这几个生命周期,下面将分别分析几个阶段的实现。
Example
1 |
|
1 | gcc -luv helloworld.c -o helloworld |
1 | $ ./helloworld |
以上是一个最基本的 libuv 程序代码,通过 uv_run 函数启动了 libuv 事件循环,所以 uv_run 做为事件循环的入口一定是阅读源码的重点,可以 uv_run 为起点,先看看 uv_run 都做了什么。
程序启动之后,打印了 quit.\n 立刻退出了,更具体一点说就是,所以函数调用尤其是 uv_run 函数立即返回了,程序自然就退出了,因为我们实际什么也有做,连上文提到的异步操作以及注册的回调函数都没有。
Init:uv_loop_init
在常见的使用场景中,通常都是直接调用 uv_default_loop 获取已经初始了的全局 uv_loop_t 实例,所以在分析 uv_run 之前,先看一下 uv_loop_t 初始化。
先来看一下 uv_default_loop:https://github.com/libuv/libuv/blob/v1.28.0/src/unix/loop.c#L30
1 | static uv_loop_t default_loop_struct; |
在 libuv 中存在一个全局的、静态的 uv_loop_t 实例 default_loop_struct,首次获取的时候经过 uv_loop_init 进行了初始化。
uv_default_loop 调用 uv_loop_init 对 default_loop_struct 进行初始化并将地址赋给了 default_loop_ptr。
uv_loop_init 实现如下(含注释):
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/loop.c#L29
1 | int uv_loop_init(uv_loop_t* loop) { |
uv_loop_init 的初始化代码是比较长的,它初始化了 libuv 运行时所有依赖的内容,这包括事件循环自身运行所需的内容,以及各类型 Handle 运行所需的共有内容和特定内容,这些都在 uv_loop_t 实例初始化的时候一并进行了初始化,初始化细节和很多有其他功能相关,当分析其他功能时,还会提及涉及到的该函数的部分代码块。
Run:uv_run
下面我们就来看一下 uv_run 函数都干了什么:
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L343
1 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { |
可以看到 uv_run 内部就是一个 while 循环,在 UV_RUN_ONCE 和 UV_RUN_NOWAIT 两种模式下,循环在执行一次后就会 break,一次性的,实际上没有循环。另外 在 uv__loop_alive(loop) == 0 或者 loop->stop_flag != 0 时 无法进入循环,同样循环结束,uv_run 函数返回。
该函数就如官网给出的流程图一样简单

再看看几个关键的函数调用:
uv__update_time(loop):对应图中Update loop timeuv__run_timers(loop):对应图中Run due timers,用于uv_timer_t,见Timeruv__run_pending(loop):对应图中Call pending callbacks,用于uv__io_t,见I/O-Watcheruv__run_idle(loop):对应图中Run idle handles,用于uv_idle_tuv__run_prepare(loop):对应图中Run prepare handles,用于uv_prepare_tuv__io_poll(loop, timeout):对应图中Poll for I/O,用于uv__io_t,见I/O-Watcheruv__run_check(loop):对应图中Run check handles,用于uv_check_tuv__run_closing_handles(loop):对应图中Call close callbacks,用于uv_handle_t,见Handle and Requst
以上执行逻辑正好和文档中的各个执行阶段相对应,文档中描述的各个执行阶段分别对应了不同的函数调用。整个循环迭代的不同阶段,对应于不同类型/状态的 handle 处理。除了用于 uv_timer_t、uv_idle_t、uv_prepare_t、uv_check_t 这四种类型的 handle 处理的几个阶段之外,没看到其他 handle 相关内容,倒是有个 uv__io_t 的处理,这是前文所提到的 libuv 内部关于I/O观察者的一个基本抽象,所有其他的 handle 都可以当做是一个I/O观察者,类似于双重继承。
如果这个函数处于一直不断的循环状态,所在进程岂不是会一直占用CPU?实际上不会这样的,因为线程会在 uv__io_poll(loop, timeout) 这个函数内部因为阻塞而挂起,挂起的时间主要由下一次到来的定时器决定。在线程挂起这段时间内,不会占用CPU。
uv_run 启动事件循环,才使所有活动状态的 handle 开始工作,否则所有 handle 都是静止的,这一步就是 libuv 启动的按钮。
事件循环自身存在存活状态,通过 uv__loop_alive 判断,实现如下:
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L331
1 | static int uv__loop_alive(const uv_loop_t* loop) { |
uv__loop_alive 判断 loop 是否是存活状态,满足以下三种条件之一即是存活状态:
- 存在活跃的 handle
- 存在活跃的 request
- 正在关闭的 handle 列表不为空
所以,若想成功事件事件循环一直不断的运行而不退出,必须在 uv_run 之前想事件循环里放入处于活跃状态的 handle 或 request。
在 uv_loop_t 结构中,存在记录处于活动状态的 handle 和 request 的计数器,所以通过简单的判断数量即可,实现如下:
1 |
1 |
另外,除了存活状态之外,loop 还存在一个 stop_flag 字段 标识 loop 是否处于关闭状态。
所以,当 loop 中没有活动的 handle 和 request 时 或者 关闭标识开启时,事件循环跳出。
libuv 在运行时 有三种模式:对应模式的用途看文档上对应的描述即可,uv_run。
在 Run 的过程中,多次调用 uv__update_time 来更新时间
https://github.com/libuv/libuv/blob/v1.x/src/unix/internal.h#L288
1 | UV_UNUSED(static void uv__update_time(uv_loop_t* loop)) { |
1 | uint64_t uv__hrtime(uv_clocktype_t type) { |
这个函数通过调用 gethrtime 获取系统当前时间,精度非常高,单位是纳秒(ns),1纳秒等于十亿分之一秒。除 1000000 后的时间单位为 毫秒(ms)。
时间对 libuv 来说非常重要,很多机制依赖于这个时间,比如定时器,后续的分析中,我们将会看到相关的利用。
在事件循环中,还有一个 timeout,这个值用于控制 uv__io_poll(loop, timeout) 的挂起时长,这个变量的值是通过 uv_backend_timeout 来获取的,源码如下:
https://github.com/libuv/libuv/blob/v1.28.0/src/unix/core.c#L311
1 | int uv_backend_timeout(const uv_loop_t* loop) { |
uv_backend_timeout 在多个情况下都返回 0,这些情况表明不需要等待超时,如果前面的条件都不满足,会通过 uv__next_timeout 计算 timeout,源码如下:
https://github.com/libuv/libuv/blob/v1.28.0/src/timer.c#L137
1 | int uv__next_timeout(const uv_loop_t* loop) { |
uv__next_timeout 有两种 情况:
- 堆为空,返回
-1 - 堆非空,返回
堆顶定时器和当前时间的差值,但是差值不能越界。
综合在一起,uv_backend_timeout 有可能返回 -1 0 正整数。
可以看到 timeout 作为参数传递给了 uv__io_poll,而 timeout 正好作为 epoll_pwait 的超时时间,所以,这个 timeout 的作用主要是使 epoll_pwait 能够有一个合理的超时时间:
- 当
timeout为-1的时候这个函数会无限期的阻塞下去; - 当
timeout为0的时候,就算没有任何事件,也会立刻返回,当没有任何需要等待的资源时,timeout刚好为0; - 当
timeout等于正整数的时候,将会阻塞timeout毫秒,或有I/O事件产生。
epoll_pwait 要在定时器时间到来时返回以进入下一次事件循环处理定时器,如果不能返回,将会导致定时任务不能按时得到处理,即使是按时返回,也不一定能够那是处理,因为 uv__io_poll 之后还有其他逻辑代码要执行,甚至是有可能是耗时计算,所以,Node.js 中定时器是不精确的,浏览器中类似。
在 UV_RUN_ONCE 模式下,因为循环会直接跳出,不会再次进入循环处理定时器,所以需要在这种模式下,需要处理额外处理定时器。
至此,事件循环的大逻辑已经分析完成了,后续,将会在各类型的 handle 的处理逻辑中展开对事件循环各阶段的内容分析。
Stop
Stop 将使事件循环在下一次循环因不满条件而无法进入,源码如下:
https://github.com/libuv/libuv/blob/v1.28.0/src/uv-common.c#L517
1 | void uv_stop(uv_loop_t* loop) { |
调用 uv_stop 后,事件循环同样无法进入,程序退出。
以上,我们分析了事件循环执行的流程。