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 和其他实体提供了高度的抽象 handles 和 streams ;它也提供了跨平台的文件 I/O 和线程功能;它还提供了更多东西。
下面的架构图说明了组成 libuv 的各个部分和子系统:

handles 和 requests
libuv 提供了组成事件轮询的两个抽象:handles 和 requests。
handles 指长期存活的对象,在活跃时能够处理指定操作,比如:
每次事件轮询时,活跃的
prepare handle的回调都会被调用。每当有新连接到来时,TCP 服务端的
handle的连接回调都会被调用。
requests 更多时候指的是短期存活的操作。这些操作都是通过 handle 来生效的:写请求是通过 handle 去写数据;或者无需 handle,getaddrinfo 请求直接运行在 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 操作。
为了更好地理解事件轮询的运行机制,下图说明了一次事件轮询中的所有阶段:

更新
loop的时间戳。在每次事件轮询开始时,loop都会缓存当前时间;这是为了减少时间相关的系统调用次数。如果
loop已经不活跃了,则立刻退出。如何判断loop的活跃状态呢?只要loop中还有活跃的和被引用的handle、活跃的request、关闭中的handle,都认为loop是活跃的。所有到期的定时器都被执行。所有时间已超过
loop时间戳的活跃的定时器的回调都被调用。pending的回调被调用。大部分 I/O 回调是在 I/Opoll后调用的;但那些在上一轮事件轮询中需要延迟到下一轮事件轮询的 I/O 回调,就是在这时候被调用。idle handle的回调被调用。在每次事件轮询中,活跃的idle handle都会运行。prepare handle的回调被调用。prepare handle的回调是在 I/Opoll之前被调用的。计算
poll的超时时间。在阻塞 I/O 之前,loop需要计算它该阻塞多久。计算超时时间的规则如下:
超时时间是 0:如果
loop以UV_RUN_NOWAIT模式运行。超时时间是 0:如果
loop已被停止(uv_stop() 被调用了)。超时时间是 0:没有活跃的
handle或活跃的request。超时时间是 0:存在活跃的
idle handle。超时时间是 0:存在需要延迟关闭的
handle。超时时间是最近的定时器的时间:如果有活跃的定时器,否则超时时间是无限。
loop阻塞 I/O。此时loop阻塞上一步计算得到超时时间。然后,所有 I/O 相关的handle的读/写回调都被调用。check handle的回调被调用。check handle的回调是在 I/Opoll之后被调用的。check handle和prepare handle正是loop中的对等部分。关闭回调被调用。uv_close() 关闭的
handle的关闭回调被调用。以
UV_RUN_ONCE模式运行的事件轮询中,I/Opoll之后可能没有 I/O 回调被调用,但到期的定时器的回调会被调用。轮询结束。如果
loop运行在UV_RUN_NOWAIT或UV_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 函数(
getaddrinfo和getnameinfo)通过 uv_queue_work() 执行的代码
版权声明: 本文为 InfoQ 作者【Huayra】的原创文章。
原文链接:【http://xie.infoq.cn/article/75aab450f43d8f6bd2ae96cab】。未经作者许可,禁止转载。











评论