一、概述
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 ) 循环的运行
为了更好的理解事件循环操作如何进行,下图展示了一个循环迭代的所有阶段。
- 循坏中的” now “被更新为当前时间(单位:纳秒)。事件循环保存“循环开始时”的时间滴答数的目的是为了减少时间相关的系统调用次数,比如在计算定时器是否到期时。
- 如果循环是 alive 的,那么表明一个迭代已经开始了,否则的话循环会立即退出。那么,什么时候一个循环被认为是 alive 的呢?答案是如果一个循环中存在活跃及被引用的 handle ( active and ref’d handles, loop 的
active_handles
计数),活跃的 request (loop 的active_reqs
队列)或者是正在关闭的 handle ( loop 的closing_handles
队列),那么这个循环被认为是 alive 的。 - 运行到期的( due )定时器。所有定时器的到期时间基于循环中的” now “,一旦到期则会调用定时器的回调函数。
- 调用挂起的( pending )回调。大部分时候, I/O 回调在 I/O 轮询( poll )之后马上被调用,但有时回调会延迟到循环的下一次迭代。该步骤就是调用上一次迭代产生的回调。(Q: 会将哪些回调挂起至下一次循环再调用?为什么要挂起?哪些是处于跨平台考虑才挂起?)
- 调用空闲/空转( idle ) handle 回调。Idle 这个名字让人遗憾,活动的空闲/空转 handle 会在循环每次迭代时执行。
- 调用准备( prepare )回调 handle 。”准备回调”在阻塞 I/O 之前被调用(即
uv__io_poll
被调用之前)。 - 计算 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_s
、uv_prepare_s
和 uv_check_s
有类似的结构和相似的函数实现。idle 会参与到超时计算中;idle 和 prepare 会在阻塞之前运行 ,而 check 会在阻塞之后运行。 check 见 9 )
⑤ 如果存在任何等待关闭的 handle ,则超时时间为 0 。关于”等待关闭的 handle “,见 10 。
⑥ 非上述情况,并且没有活动的或接近超时的定时器,则超时时间为 -1 ,表示一直阻塞。
- 阻塞 I/O 。循环阻塞上一步计算出来的超时时间那么久。所有监视指定文件描述符的读可操作的 handle 的回调在这里被调用。
- 调用检查( check ) handle 回调。”检查回调”在阻塞 I/O 之后被调用。本质上对应于”准备回调”。
- 调用关闭( close ) handle 回调。如果 handle 被
uv_close
函数关闭(并指定了回调),则该回调在这里被调用。(为什么不直接在uv_close
函数或uv_XXX_close
函数中直接调用关闭回调?) - 特别地,如果循环的运行模式为
UV_RUN_ONCE
,as it implies forward progress 。可能在阻塞 I/O (超时)后没执行任何 handle 回调,但时间已经过去,所以可能存在到期的定时器,则这些定时器的回调函数得以调用。 - 迭代结束。如果循环的运行模式为
UV_RUN_NOWAIT
或UV_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 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { |
四、问题
- 会将哪些回调挂起至下一次循环再调用?为什么要挂起?哪些是处于跨平台考虑才挂起?