理解 libuv 运行机制

2022年02月07日

libuv 作为 Node.js 很重要的一个依赖,为其引入了事件循环(Event Loop)的机制,统一了 Linux、Mac 和 Windows 等各个平台非阻塞 IO 操作接口,且提供了以一些功能:包括操作文件、DNS、网络、子进程、管道、信号处理等等。

本篇文章就深入探究一下其内部的运行机制。

libuv 代码中的两个抽象

为了与事件循环机制相结合,libuv 提供了两个抽象:handles 和 requests。

当处于激活状态时,handles 代表长生命周期的一些操作,例如:

而 request 一般仅代表短生命周期的操作。可以通过 handle 来执行操作:write requests 需要 handle 来执行写操作;也可独立执行:getaddrinfo requests 直接在循环中执行。

Event Loop

I/O 循环(Event Loop)是 libuv 的核心部分。所有的 I/O 操作都在事件循环当中,事件循环应该被绑定在单个线程上。所以如果想要在多线程上跑的话,应该每个线程都引入事件循环。

基本上各个平台都会支持异步非阻塞的套接字,libuv 会根据平台的不同,去引入对应的最好的机制:Linux 是 epoll、OSX 是 kqueue、Windows 是 IOCP。作为循环的组成部分,网络套接字在空闲时会阻塞住,直到套接字变得可读或者可写,然后事件循环便继续进行。

为了更好地理解,下图展示了循环的所有阶段:

image-20220207191033564

可阅读代码 src/unix/core.c uv_run 方法,更好地理解上面的流程图。

事件循环核心流程描述如下:

  1. 更新循环阶段的当前时间。在每次循环的开始阶段,都会缓存当前的时间,接下来循环中都会使用此时间,目的是为了减少系统调用。
  2. 判断当前循环是否处于活跃状态,如果非活跃状态则直接退出循环。那么,什么情况下循环是活跃状态呢?当循环关联有活跃的 handles 时、或活跃的 requests 时、或存在 closing hanldes 时,循环可被认为是活跃的。
  3. 执行 timer 阶段。所有早于当前时间的 timers 可被执行,它们关联的回调函数将被调用。
  4. 执行 pending callbacks 阶段。
  5. 执行 idle handle 阶段。如果有存在活跃状态的 idle 阶段,它们将在每个循环中被执行。
  6. 执行 prepare handle 阶段。在循环阻塞 I/O 前,会执行此阶段。
  7. 计算 poll timeout。在 I/O 阻塞前,计算应该被阻塞多久。有下面的几条规则用于计算 timeout:
    1. 如果 loop 是 UV_RUN_NOWAIT 方式,则 timeout 为 0
    2. 如果 loop 将要被停止(调用过 uv_stop()),则 timeout 为 0
    3. 如果没有活跃的 handles 或 requests,则 timeou 为 0
    4. 如果存在任一活跃 idle handle,则 timeout 为 0
    5. 如果存在正在关闭的 handles,则 timeout 为 0
    6. 上面的情况不满足,取最近的到期 timer 作为 timeout,如果无 timer,则 timeout 为无穷
  8. 循环阻塞在 I/O。在此阶段,循环一直阻塞,直到上一步计算的 timeout 到达。当有文件描述符变得可读或者可写,将会在此阶段调用它们关联的回调函数。
  9. 执行 check handle 阶段。
  10. 调用 close 回调。如果 handle 被调用 uv_close(),则会在此调用回调。
  11. 如果循环是 UV_RUN_ONCE 方式启动,将会执行 timer 回调。
  12. 此次循环结束。如果是以 UV_RUN_NOWAIT 或 UV_RUN_ONCE 方式启动的循环,则退出循环。如果是以 UV_RUN_DEFAULT 开始的循环(Node.js 主线程为此状态),如果处于活跃状态则会再次从头进入循环(跳到第一步),否则退出循环。

本地安装 libuv

1git clone git@github.com:libuv/libuv.git
2cd libuv
3./autogen.sh
4./configure
5make
6make install

安装之后就会有 /usr/local/include/uv.h 和 /usr/local/lib/libuv.a 文件

编译代码时执行 gcc main.c -luv 即可

核心代码注释

以下基于 libuv-1.43.0 版本,src/unix/core.c uv_run 代码注释

 1int uv_run(uv_loop_t* loop, uv_run_mode mode) {
 2  int timeout;
 3  int r;
 4  int ran_pending;
 5
 6  r = uv__loop_alive(loop); // 判断循环是否活跃
 7  if (!r)
 8    uv__update_time(loop);
 9
10  while (r != 0 && loop->stop_flag == 0) { // 如果循环活跃且没被手动停止,进入循环
11    uv__update_time(loop); // 更新当前时间,减少系统调用
12    uv__run_timers(loop); // 执行 timers 阶段
13    ran_pending = uv__run_pending(loop); // 执行 pending 阶段
14    uv__run_idle(loop); // 执行 idle 阶段
15    uv__run_prepare(loop); // 执行 prepare 阶段
16
17    timeout = 0;
18    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
19      timeout = uv_backend_timeout(loop); // 计算 timeout
20
21    uv__io_poll(loop, timeout); // 进入 io 阻塞阶段,传入上一步计算出的 timeout
22
23    uv__metrics_update_idle_time(loop);
24
25    uv__run_check(loop); // 执行 check 阶段
26    uv__run_closing_handles(loop); // 处理 close 相关回调
27
28    if (mode == UV_RUN_ONCE) {
29      uv__update_time(loop);
30      uv__run_timers(loop);
31    }
32
33    r = uv__loop_alive(loop); // 判断循环是否活跃
34    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
35      break; // 如果是 UV_RUN_ONCE 和 UV_RUN_NOWAIT,跳出循环
36  }
37
38  if (loop->stop_flag != 0)
39    loop->stop_flag = 0;
40
41  return r;
42}

参考