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】。未经作者许可,禁止转载。
评论