写点什么

libuv 异步模型之设计概览

用户头像
Huayra
关注
发布于: 2020 年 08 月 14 日
libuv 异步模型之设计概览

在深入 uv__io 之前,先了解一下 libuv 的整体架构 http://docs.libuv.org/en/v1.x/design.html

以下为 libuv 官网 Design Overview 的翻译。


设计概览

libuv 是跨平台的支持库,原先只是为 Node.js 而写。它围绕着事件驱动的异步 I/O 模型而设计。



在不同的 I/O poll 机制基础上,它提供了非常简单的抽象。它为 socket 和其他实体提供了高度的抽象 handlesstreams ;它也提供了跨平台的文件 I/O 和线程功能;它还提供了更多东西。



下面的架构图说明了组成 libuv 的各个部分和子系统:

handles 和 requests

libuv 提供了组成事件轮询的两个抽象:handlesrequests

handles 指长期存活的对象,在活跃时能够处理指定操作,比如:

  • 每次事件轮询时,活跃的 prepare handle 的回调都会被调用。

  • 每当有新连接到来时,TCP 服务端的 handle 的连接回调都会被调用。

requests 更多时候指的是短期存活的操作。这些操作都是通过 handle 来生效的:写请求是通过 handle 去写数据;或者无需 handlegetaddrinfo 请求直接运行在 loop 上。

I/O 轮询

I/O 轮询(I/O 事件)是 libuv 的核心部分。它构成了所有的 I/O 操作,这意味着它是单线程的。多个事件轮询意味着多线程。除了状态信息,libuv 的事件轮询,主要指轮询或 handle 相关的 API,不是线程安全的



事件轮询遵循着单线程异步 I/O 模型:所有的(网络)I/O 都是基于非阻塞的 socket 来生效,并采用了特定平台特定 poll 模型:Linux 的 epoll,OSX 和其他 BSD 的 kqueue,SunOS 的 event ports 和 Windows 的 IOCP。作为事件轮询的一部分,loop 会阻塞在 I/O socket 的等待上;已添加到 poll 模型的 I/O socket 的回调将在 socket 条件(可读/可写条件)触发时被调用,从而使得那些 handle 去读写数据,或执行 I/O 操作。



为了更好地理解事件轮询的运行机制,下图说明了一次事件轮询中的所有阶段:





  1. 更新 loop 的时间戳。在每次事件轮询开始时,loop 都会缓存当前时间;这是为了减少时间相关的系统调用次数。

  2. 如果 loop 已经不活跃了,则立刻退出。如何判断 loop 的活跃状态呢?只要 loop 中还有活跃的和被引用的 handle 、活跃的 request 、关闭中的 handle ,都认为 loop 是活跃的。

  3. 所有到期的定时器都被执行。所有时间已超过 loop 时间戳的活跃的定时器的回调都被调用。

  4. pending 的回调被调用。大部分 I/O 回调是在 I/O poll 后调用的;但那些在上一轮事件轮询中需要延迟到下一轮事件轮询的 I/O 回调,就是在这时候被调用。

  5. idle handle 的回调被调用。在每次事件轮询中,活跃的 idle handle 都会运行。

  6. prepare handle 的回调被调用。prepare handle 的回调是在 I/O poll 之前被调用的。

  7. 计算 poll 的超时时间。在阻塞 I/O 之前,loop 需要计算它该阻塞多久。计算超时时间的规则如下:

  • 超时时间是 0:如果 loopUV_RUN_NOWAIT 模式运行。

  • 超时时间是 0:如果 loop 已被停止(uv_stop() 被调用了)。

  • 超时时间是 0:没有活跃的 handle 或活跃的 request

  • 超时时间是 0:存在活跃的 idle handle

  • 超时时间是 0:存在需要延迟关闭的 handle

  • 超时时间是最近的定时器的时间:如果有活跃的定时器,否则超时时间是无限。

  1. loop 阻塞 I/O。此时 loop 阻塞上一步计算得到超时时间。然后,所有 I/O 相关的 handle 的读/写回调都被调用。

  2. check handle 的回调被调用。check handle 的回调是在 I/O poll 之后被调用的。check handleprepare handle 正是 loop 中的对等部分。

  3. 关闭回调被调用。uv_close() 关闭的 handle 的关闭回调被调用。

  4. UV_RUN_ONCE 模式运行的事件轮询中,I/O poll 之后可能没有 I/O 回调被调用,但到期的定时器的回调会被调用。

  5. 轮询结束。如果 loop 运行在 UV_RUN_NOWAITUV_RUN_ONCE 模式下,轮询结束后 uv_run() 就返回了。如果 loop 运行在 UV_RUN_DEFAULT 模式下,将会进行下一次轮询。



注意:libuv 使用线程池去执行异步文件 I/O 操作,使用 loop 所在的线程去处理网络 I/O 操作。



不同平台会有不同的 poll 机制,但 libuv 为 Unix 系统和 Windows 保持了一致的运行模型。

文件 I/O

不像网络 I/O,libuv 没有平台相关的文件 I/O 机制可依赖;所以现行的方式是在线程池中处理阻塞的文件 I/O。



请阅读博客:https://blog.libtorrent.org/2012/10/asynchronous-disk-io/ 去理解跨平台文件 I/O 设计。



libuv 现在使用全局线程池去执行以下 3 类操作:

  • 文件系统操作

  • DNS 函数(getaddrinfogetnameinfo

  • 通过 uv_queue_work() 执行的代码



发布于: 2020 年 08 月 14 日阅读数: 85
用户头像

Huayra

关注

Gopher & Pythonista 2017.12.12 加入

还未添加个人简介

评论

发布
暂无评论
libuv 异步模型之设计概览