Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[译] libuv 设计概述 #34

Open
DavidCai1111 opened this issue Jul 4, 2016 · 0 comments
Open

[译] libuv 设计概述 #34

DavidCai1111 opened this issue Jul 4, 2016 · 0 comments
Labels

Comments

@DavidCai1111
Copy link
Owner

概述

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

@DavidCai1111 DavidCai1111 changed the title [译]libuv 设计概述 [译] libuv 设计概述 Jul 4, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant