[译] libuv 设计概述

概述

libuv 最初是为 Node.js 所作的跨平台库。它基于事件驱动的异步 I/O 模型。

libuv 不仅仅只提供了对于不同 I/O 轮询机制的简单抽象:“句柄(handles)”和“流(streams)”也提供了对于 socket 和其他相关实例的高度抽象。同时 libuv 还提供了跨平台文件 I/O 接口和多线程接口等等。

下图展示了 libuv 的不同组成部分,以及与这些部分相关的子模块:

architecture.png

句柄(handles)和请求(requests)

为了能使用户介入事件循环(event loop),libuv 为用户提供了两个抽象:句柄和请求。

句柄表示一个在其被激活时可以执行某些操作且持久存在的对象。例如:当一个预备句柄(prepare handle)处于激活时,它的回调函数会在每次事件循环中被调用;每当一个新 TCP 连接来到时,一个 TCP 服务器句柄的连接回调函数就会被调用。

请求(通常)表示一个短暂存在的操作。这些操作可以操作于句柄,例如写请求(write requests)用于向一个句柄写入数据。但是又如 getaddrinfo 请求则不依赖于一个句柄,它们直接在事件循环上执行。

事件循环

事件循环是 libuv 的核心部分。它为所有的 I/O 操作建立了上下文,并且执行于一个单线程中。你可以在多个不同的线程中运行多个事件循环。除非另有说明,不然 libuv 的事件循环(以及其他循环或句柄提供的 API)并不是线程安全的

事件循环遵循着普遍的单线程异步 I/O 行为:所有的(网络)I/O 体现在非阻塞的 socket 上,对于不同的平台,libuv 会选取最佳的轮询机制:Linux 上为 epoll ,OSX 和其他 BSD 上为 kqueue ,SunOS 上为 event ports , Windows 上则为 IOCP 。作为循环迭代的一部分,事件循环会阻塞并等待被添加的 socket 上 I/O 活动的发生。然后根据当前的 socket 情况(可读,可写,挂起)触发相应的回调函数。所以,一个句柄是可以执行读操作,写操作或其他 I/O 行为。

为了能更好的理解事件循环是如何工作的,下图展示了事件循环一次迭代的所有过程:

loop_iteration.png

  1. 事件循环中的“现在时间(now)”被更新。事件循环会在一次循环迭代开始的时候缓存下当时的时间,用于减少与时间相关的系统调用次数。

  2. 如果事件循环仍是存活(alive)的,那么迭代就会开始,否则循环会立刻退出。如果一个循环内包含激活的可引用句柄,激活的请求或正在关闭的句柄,那么则认为该循环是存活的。

  3. 执行超时定时器(due timers)。所有在循环的“现在时间”之前超时的定时器都将在这个时候得到执行。

  4. 执行等待中回调(pending callbacks)。正常情况下,所有的 I/O 回调都会在轮询 I/O 后立刻被调用。但是有些情况下,回调可能会被推迟至下一次循环迭代中再执行。任何上一次循环中被推迟的回调,都将在这个时候得到执行。

  5. 执行闲置句柄回调(idle handle callbacks)。尽管它有个不怎么好听的名字,但只要这些闲置句柄是激活的,那么在每次循环迭代中它们都会执行。

  6. 执行预备回调(prepare handle)。预备回调会在循环为 I/O 阻塞前被调用。

  7. 开始计算轮询超时(poll timeout)。在为 I/O 阻塞前,事件循环会计算它即将会阻塞多长时间。以下为计算该超时的规则:

    • 如果循环带着 UV_RUN_NOWAIT 标识执行,那么超时将会是 0 。
    • 如果循环即将停止(uv_stop() 已在之前被调用),那么超时将会是 0 。
    • 如果循环内没有激活的句柄和请求,那么超时将会是 0 。
    • 如果循环内有激活的闲置句柄,那么超时将会是 0 。
    • 如果有正在等待被关闭的句柄,那么超时将会是 0 。
    • 如果不符合以上所有,那么该超时将会是循环内所有定时器中最早的一个超时时间,如果没有任何一个激活的定时器,那么超时将会是无限长(infinity)。
  8. 事件循环为 I/O 阻塞。此时事件循环将会为 I/O 阻塞,持续时间为上一步中计算所得的超时时间。所有与 I/O 相关的句柄都将会监视一个指定的文件描述符,等待一个其上的读或写操作来激活它们的回调。

  9. 执行检查句柄回调(check handle callbacks)。在事件循环为 I/O 阻塞结束后,检查句柄的回调将会立刻执行。检查句柄本质上是预备句柄的对应物(counterpart)。

  10. 执行关闭回调(close callbacks)。如果一个句柄通过调用 uv_close() 被关闭,那么这将会调用关闭回调。

  11. 尽管在为 I/O 阻塞后可能并没有 I/O 回调被触发,但是仍有可能这时已经有一些定时器已经超时。若事件循环是以 UV_RUN_ONCE 标识执行,那么在这时这些超时的定时器的回调将会在此时得到执行。

  12. 迭代结束。如果循环以 UV_RUN_NOWAITUV_RUN_ONCE 标识执行,迭代便会结束,并且 uv_run() 将会返回。如果循环以 UV_RUN_DEFAULT 标识执行,那么如果若它还是存活的,它就会开始下一次迭代,否则结束。

重要:虽然 libuv 的异步文件 I/O 操作是通过线程池实现的,但是网络 I/O 总是在单线程中执行的。

注意:虽然在不同平台上使用的轮询机制不同,但 libuv 的执行模型在不同平台下都是保持一致。

文件 I/O

与网络 I/O 不同,并不存在 libuv 可以依靠的各特定平台下的文件 I/O 基础函数,所以目前的实现是在线程中执行阻塞的文件 I/O 操作来模拟异步。

更多关于跨平台异步文件 I/O 操作的内容,可参阅此文

libuv 目前使用了一个全局的线程池,所有的循环都可以往其中加入任务。目前有三种操作会在这个线程池中执行:

  • 文件系统操作
  • DNS 函数(getaddrinfo 和 getnameinfo)
  • 通过 uv_queue_work() 添加的用户代码

注意:更多关于 libuv 线程池的信息请参阅此文。请牢记线程池的大小是有限的。

最后

原文链接:http://docs.libuv.org/en/v1.x/design.html