Alby's blog

世上没有巧合,只有巧合的假象。

0%

Libuv 源码分析(8):I/O ( 或 event ) 循环的运行

一、概述

Libuv 官方文档( https://github.com/libuv/libuv/tree/v1.x/docs )和 uvbook ( http://nikhilm.github.com/uvbook/ ) 对 I/O ( event ) 循环有比较详细的介绍。
本文首先摘抄/抄袭了网络上对官方文档关于 I/O( event ) 描述的翻译,并做了必要的补充。

二、I/O ( 或 event ) 循环

I/O 循环是 Libuv 的核心。它建立了所有 I/O 操作的执行环境,并且被绑定在一个线程之上。我们可以运行多个事件循环,只要每一个都运行在不同的线程之上。Libuv 事件循环(包括其他与事件循环相关的 API 或句柄 )不是线程安全的,除非另有说明。
事件循环遵循比较普通的单线程异步 I/O 方法:所有(网络) I/O 操作在非阻塞的 socket 上执行,使用给定平台上可用的最佳机制进行轮询( I/O 复用):在 Linux上使用 epoll ,在 macOS 和其他 BSD 平台上使用 kqueue ,在 sunOS 上使用 event ports,在 Windows 上使用 IOCP。在循环的每次迭代中,会阻塞等待 socket 上的 I/O 活动。一旦可读可写等事件发生,将会调用注册到循环中的回调。

三、I/O ( 或 event ) 循环的运行

为了更好的理解事件循环操作如何进行,下图展示了一个循环迭代的所有阶段。

图1: I/O ( 或 event ) 循环。
  1. 循坏中的” now “被更新为当前时间(单位:纳秒)。事件循环保存“循环开始时”的时间滴答数的目的是为了减少时间相关的系统调用次数,比如在计算定时器是否到期时。
  2. 如果循环是 alive 的,那么表明一个迭代已经开始了,否则的话循环会立即退出。那么,什么时候一个循环被认为是 alive 的呢?答案是如果一个循环中存在活跃及被引用的 handle ( active and ref’d handles, loop 的 active_handles 计数),活跃的 request (loop 的 active_reqs 队列)或者是正在关闭的 handle ( loop 的 closing_handles 队列),那么这个循环被认为是 alive 的。
  3. 运行到期的( due )定时器。所有定时器的到期时间基于循环中的” now “,一旦到期则会调用定时器的回调函数。
  4. 调用挂起的( pending )回调。大部分时候, I/O 回调在 I/O 轮询( poll )之后马上被调用,但有时回调会延迟到循环的下一次迭代。该步骤就是调用上一次迭代产生的回调。(Q: 会将哪些回调挂起至下一次循环再调用?为什么要挂起?哪些是处于跨平台考虑才挂起?)
  5. 调用空闲/空转( idle ) handle 回调。Idle 这个名字让人遗憾,活动的空闲/空转 handle 会在循环每次迭代时执行。
  6. 调用准备( prepare )回调 handle 。”准备回调”在阻塞 I/O 之前被调用(即 uv__io_poll 被调用之前)。
  7. 计算 poll 超时时间。如果 poll 一直阻塞,则 loop 本身也一直出于阻塞状态,定时器等其他一次操作将无法获得或再次获得调用;反过来,如果 poll 不阻塞(类似于循环中仅有 idle handle 的情况),对 CPU 是种极大的浪费。所以需将 poll 超时时间设置成一个合适的值。”计算 poll 超时时间”在阻塞 I/O 之前被调用。下面是超时时间的计算规则:

① 如果循环的运行模式为 UV_RUN_NOWAIT ,则超时时间为 0 。
② 如果循环将要停止(调用 uv_stop 函数,该函数将循环结构的 stop_flag 设置为 1 。), 则超时时间为 0 。
③ 如果没有活动的 handle 和 request , 则超时时间为 0 。
④ 如果存在任何活动的空转( idle ) handle,则超时时间为 0 。( uv_idle_suv_prepare_suv_check_s 有类似的结构和相似的函数实现。idle 会参与到超时计算中;idle 和 prepare 会在阻塞之前运行 ,而 check 会在阻塞之后运行。 check 见 9 )
⑤ 如果存在任何等待关闭的 handle ,则超时时间为 0 。关于”等待关闭的 handle “,见 10 。
⑥ 非上述情况,并且没有活动的或接近超时的定时器,则超时时间为 -1 ,表示一直阻塞。

  1. 阻塞 I/O 。循环阻塞上一步计算出来的超时时间那么久。所有监视指定文件描述符的读可操作的 handle 的回调在这里被调用。
  2. 调用检查( check ) handle 回调。”检查回调”在阻塞 I/O 之后被调用。本质上对应于”准备回调”。
  3. 调用关闭( close ) handle 回调。如果 handle 被 uv_close 函数关闭(并指定了回调),则该回调在这里被调用。(为什么不直接在 uv_close 函数或 uv_XXX_close 函数中直接调用关闭回调?)
  4. 特别地,如果循环的运行模式为 UV_RUN_ONCE ,as it implies forward progress 。可能在阻塞 I/O (超时)后没执行任何 handle 回调,但时间已经过去,所以可能存在到期的定时器,则这些定时器的回调函数得以调用。
  5. 迭代结束。如果循环的运行模式为 UV_RUN_NOWAITUV_RUN_ONCE ,迭代结束并且 uv_run 函数返回;如果循环的运行模式为 UV_RUN_DEFAULT , 循环仍然 alive 则继续迭代,否则迭代结束并且 uv_run 函数返回。

重要:Libuv 利用线程池技术使得异步文件 I/O 操作成为可能,但是对于网络 I/O 总是在单个线程中执行,即每个 loop 线程。

虽然轮询机制不同, 但 Libuv 使执行模型在 UNIX 系统和 Windows 之间保持一致。

I/O 循环由 uv_run 函数封装, 在使用 Libuv 编程时, 该函数通常在最后才被调用。uv_run 位于源码 src/unix/core.c ( 或 src/win/core.c ) 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;

r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);

while (r != 0 && loop->stop_flag == 0) { // 2
uv__update_time(loop); // 1
uv__run_timers(loop); // 3
ran_pending = uv__run_pending(loop); // 4
uv__run_idle(loop); // 5
uv__run_prepare(loop); // 6

timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop); // 7

uv__io_poll(loop, timeout); // 8
uv__run_check(loop); // 9
uv__run_closing_handles(loop); // 10

if (mode == UV_RUN_ONCE) { // 11
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop);
}

r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) // 12
break;
}

/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0;

return r;
}

四、问题

  1. 会将哪些回调挂起至下一次循环再调用?为什么要挂起?哪些是处于跨平台考虑才挂起?

参考资料