[译] libuv 设计概述
evcb8621
8年前
<h2>概述</h2> <p>libuv 最初是为 Node.js 所作的跨平台库。它基于事件驱动的异步 I/O 模型。</p> <p>libuv 不仅仅只提供了对于不同 I/O 轮询机制的简单抽象:“句柄(handles)”和“流(streams)”也提供了对于 socket 和其他相关实例的高度抽象。同时 libuv 还提供了跨平台文件 I/O 接口和多线程接口等等。</p> <p>下图展示了 libuv 的不同组成部分,以及与这些部分相关的子模块:</p> <p><img alt="" src="https://simg.open-open.com/show/4c74fa1bdf6488810f677326e8dd3d07.png"></p> <h2>句柄(handles)和请求(requests)</h2> <p>为了能使用户介入事件循环(event loop),libuv 为用户提供了两个抽象:句柄和请求。</p> <p>句柄表示一个在其被激活时可以执行某些操作且持久存在的对象。例如:当一个预备句柄(prepare handle)处于激活时,它的回调函数会在每次事件循环中被调用;每当一个新 TCP 连接来到时,一个 TCP 服务器句柄的连接回调函数就会被调用。</p> <p>请求(通常)表示一个短暂存在的操作。这些操作可以操作于句柄,例如写请求(write requests)用于向一个句柄写入数据。但是又如 getaddrinfo 请求则不依赖于一个句柄,它们直接在事件循环上执行。</p> <h2>事件循环</h2> <p>事件循环是 libuv 的核心部分。它为所有的 I/O 操作建立了上下文,并且执行于一个单线程中。你可以在多个不同的线程中运行多个事件循环。除非另有说明,不然 libuv 的事件循环(以及其他循环或句柄提供的 API) <strong>并不是线程安全的</strong> 。</p> <p>事件循环遵循着普遍的单线程异步 I/O 行为:所有的(网络)I/O 体现在非阻塞的 socket 上,对于不同的平台,libuv 会选取最佳的轮询机制:Linux 上为 epoll ,OSX 和其他 BSD 上为 kqueue ,SunOS 上为 event ports , Windows 上则为 IOCP 。作为循环迭代的一部分,事件循环会阻塞并等待被添加的 socket 上 I/O 活动的发生。然后根据当前的 socket 情况(可读,可写,挂起)触发相应的回调函数。所以,一个句柄是可以执行读操作,写操作或其他 I/O 行为。</p> <p>为了能更好的理解事件循环是如何工作的,下图展示了事件循环一次迭代的所有过程:</p> <p><img alt="" src="https://simg.open-open.com/show/c968d68241b6bdc4e233b38d752cef97.png"></p> <ol> <li> <p>事件循环中的“现在时间(now)”被更新。事件循环会在一次循环迭代开始的时候缓存下当时的时间,用于减少与时间相关的系统调用次数。</p> </li> <li> <p>如果事件循环仍是存活(alive)的,那么迭代就会开始,否则循环会立刻退出。如果一个循环内包含激活的可引用句柄,激活的请求或正在关闭的句柄,那么则认为该循环是存活的。</p> </li> <li> <p>执行超时定时器(due timers)。所有在循环的“现在时间”之前超时的定时器都将在这个时候得到执行。</p> </li> <li> <p>执行等待中回调(pending callbacks)。正常情况下,所有的 I/O 回调都会在轮询 I/O 后立刻被调用。但是有些情况下,回调可能会被推迟至下一次循环迭代中再执行。任何上一次循环中被推迟的回调,都将在这个时候得到执行。</p> </li> <li> <p>执行闲置句柄回调(idle handle callbacks)。尽管它有个不怎么好听的名字,但只要这些闲置句柄是激活的,那么在每次循环迭代中它们都会执行。</p> </li> <li> <p>执行预备回调(prepare handle)。预备回调会在循环为 I/O 阻塞前被调用。</p> </li> <li> <p>开始计算轮询超时(poll timeout)。在为 I/O 阻塞前,事件循环会计算它即将会阻塞多长时间。以下为计算该超时的规则:</p> <ul> <li> <p>如果循环带着 UV_RUN_NOWAIT 标识执行,那么超时将会是 0 。</p> </li> <li> <p>如果循环即将停止( uv_stop() 已在之前被调用),那么超时将会是 0 。</p> </li> <li> <p>如果循环内没有激活的句柄和请求,那么超时将会是 0 。</p> </li> <li> <p>如果循环内有激活的闲置句柄,那么超时将会是 0 。</p> </li> <li> <p>如果有正在等待被关闭的句柄,那么超时将会是 0 。</p> </li> <li> <p>如果不符合以上所有,那么该超时将会是循环内所有定时器中最早的一个超时时间,如果没有任何一个激活的定时器,那么超时将会是无限长(infinity)。</p> </li> </ul> </li> <li> <p>事件循环为 I/O 阻塞。此时事件循环将会为 I/O 阻塞,持续时间为上一步中计算所得的超时时间。所有与 I/O 相关的句柄都将会监视一个指定的文件描述符,等待一个其上的读或写操作来激活它们的回调。</p> </li> <li> <p>执行检查句柄回调(check handle callbacks)。在事件循环为 I/O 阻塞结束后,检查句柄的回调将会立刻执行。检查句柄本质上是预备句柄的对应物(counterpart)。</p> </li> <li> <p>执行关闭回调(close callbacks)。如果一个句柄通过调用 uv_close() 被关闭,那么这将会调用关闭回调。</p> </li> <li> <p>尽管在为 I/O 阻塞后可能并没有 I/O 回调被触发,但是仍有可能这时已经有一些定时器已经超时。若事件循环是以 UV_RUN_ONCE 标识执行,那么在这时这些超时的定时器的回调将会在此时得到执行。</p> </li> <li> <p>迭代结束。如果循环以 UV_RUN_NOWAIT 或 UV_RUN_ONCE 标识执行,迭代便会结束,并且 uv_run() 将会返回。如果循环以 UV_RUN_DEFAULT 标识执行,那么如果若它还是存活的,它就会开始下一次迭代,否则结束。</p> </li> </ol> <p>重要:虽然 libuv 的异步文件 I/O 操作是通过线程池实现的,但是网络 I/O 总是在单线程中执行的。</p> <p>注意:虽然在不同平台上使用的轮询机制不同,但 libuv 的执行模型在不同平台下都是保持一致。</p> <h2>文件 I/O</h2> <p>与网络 I/O 不同,并不存在 libuv 可以依靠的各特定平台下的文件 I/O 基础函数,所以目前的实现是在线程中执行阻塞的文件 I/O 操作来模拟异步。</p> <p>更多关于跨平台异步文件 I/O 操作的内容,可参阅 <a href="/misc/goto?guid=4959675050338698639" rel="nofollow,noindex">此文</a> 。</p> <p>libuv 目前使用了一个全局的线程池,所有的循环都可以往其中加入任务。目前有三种操作会在这个线程池中执行:</p> <ul> <li> <p>文件系统操作</p> </li> <li> <p>DNS 函数(getaddrinfo 和 getnameinfo)</p> </li> <li> <p>通过 uv_queue_work() 添加的用户代码</p> </li> </ul> <p>注意:更多关于 libuv 线程池的信息请参阅 <a href="/misc/goto?guid=4959675050432913122" rel="nofollow,noindex">此文</a> 。请牢记线程池的大小是有限的。</p> <h2> </h2> <p>来自: <a href="/misc/goto?guid=4959675050514642737" rel="nofollow">https://segmentfault.com/a/1190000005873917</a></p> <p> </p>