好好活就是有意义的事,有意义的事就是好好活
libuv 设计总览
libuv 设计总览

libuv 设计总览

原文链接: libuv 1.43.1-dev documentation

Design overview

libuv 是最初为 Node.js 编写的跨平台支持库。 它是基于事件驱动的异步 I/O 模型设计的。

该库不仅仅提供了针对不同 I/O 轮询机制的简单抽象(通过 handlesstreams为socket和其他实体提供高级抽象), 还提供了跨平台文件 I/O 和线程功能。

右图描述了组成 libuv 的不同部分以及它们相关的子系统:

注释: 对于socket, 管道, TTY等可以使用epoll轮询机制的IO操作, libuv提供了高层面的抽象; 而对于文件IO等无法进行epoll, 使用的是多线程的方式进行异步, 同时还提供了多线程的接口, 让可以让自己的代码异步执行.

Handlers and requests

libuv 结合事件循环为用户提供了 2 个可以使用的抽象:句柄(handlers)和请求(requests).

句柄表示能够在活动时执行某些操作的长期对象, 如:

  • prepare handle 在active时每次循环迭代都会调用一次回调
  • TCP server handle 在每次有新连接时都会调用其连接回调

注释: 所谓句柄, 就是那些能被epoll监控的对象, 如TCP socket, eventfd等, 所谓回调, 就是epoll被触发时的处理函数 ; 所谓长期的活动, 是TCP的socket长时间的等待连接.

Request通常代表短暂的操作。 这些操作:

  • 可以在句柄上执行:写请求用于在句柄上写入数据;
  • 或独立执行:getaddrinfo 请求不需要依托于句柄, 而是直接在循环上运行;

注释: 之所以叫请求, 是因为这不是一个连续的过程, 执行一次就会结束. 但是像写操作, getaddrinfo等都是需要异步, 前者等待写缓存空闲, 后者等待网络请求被恢复.

The I/O loop

I/O(或事件) 循环是 libuv 的核心部分, 用于异步执行所有 I/O 操作 . 事件循环被绑定到单个线程, 只要每个事件循环在不同的线程中运行,就可以运行多个事件循环。但是 libuv 事件循环(或任何其他涉及循环或句柄的 API)不是线程安全的,除非另有说明。

注释: 只要事件循环的核心结构不是共享, 每个线程各用各的, 那么就可以多线程循环, 但是如果混用, 或者同时监控一个句柄, 是无法保证线程安全的.

事件循环遵循相当常见的单线程异步 I/O 方法:所有(网络)I/O 都在非阻塞套接字上执行,这些套接字使用给定平台上可用的最佳机制进行轮询:Linux 上的 epoll,OSX 和 其他 BSD上的 kqueue 、SunOS 上的event ports和 Windows 上的 IOCP。 事件循环会阻塞等待添加轮询器的套接字上的IO活动, 当IO事件到达时, 会执行指定的回调函数, 从而完成句柄的读取, 写入等操作.

注释: 以上仅仅是适用于epoll的IO, 最典型的就是网络IO

为了更好地理解事件循环是如何运作的,下图说明了循环迭代的所有阶段:

重要:

libuv 使用线程池使异步文件 I/O 操作成为可能,但网络 I/O 始终在单个线程中执行,即每个loop的线程。

虽然轮询机制不同,但 libuv 使执行模型在 Unix 系统和 Windows 之间保持一致。

  1. 事件循环中的 ‘now’ 并不是实时的, 而是在每次循环的开始, 记录一下然后缓存起来, 这样是为了减少与时间相关的系统调用.
  2. 如果loop当前是alive的, 那么将开始循环; 否则循环将会退出. 只要loop具有active and ref’d handles, active requests 或者 closing handles , 那么就认为它是alive.
  3. 执行到点的计时器. 所有的计时器, 只要到点时间是在’now’之前的, 那么就统统执行他们的回调函数. 可见这个定时器不是那么靠谱, 毕竟是单线程.
  4. 调用挂起的回调. 在大多数情况下,在轮询 I/O 之后立即调用所有 I/O 回调。 但是,在某些情况下,调用此类回调会延迟到下一次循环迭代。 如果之前的迭代延迟了任何 I/O 回调,它将在此时运行。
  5. 调用idle句柄的回调. 尽管名字是Idle, 但是如果Idle句柄是active的, 那么每次循环迭代时都会运行它们. Idle可以保证事件循环不会退出.
  6. 调用Prepare 句柄的回调. 在loop阻塞等待IO之前调用Prepare 句柄的回调.
  7. 计算轮询的超时时间. 在阻塞 I/O 之前,循环会计算它应该阻塞多长时间, 这是非常必要的, 不然上面的一大圈特别是定时器啥时候执行, 如果一直阻塞的话, 超时时间的计算规则如下:
    • 如果循环使用 UV_RUN_NOWAIT 标志运行,则超时为 0
    • 如果要停止循环(调用了 uv_stop()),则超时为 0
    • 如果没有活动句柄或请求,则超时为 0
    • 如果有任何Idle句柄处于活动状态,则超时为 0
    • 如果有任何待关闭的句柄,则超时为 0
    • 如果以上情况都不匹配,则采用最近的计时器的超时
    • 如果没有活动的计时器,则为无穷大
  8. 阻塞等待IO时间(Poll for I/O). 此时,循环将在上一步计算的持续时间内阻塞 I/O。 所有被监视的句柄对应的文件描述符执行读取或写入时都会触发回调
  9. 调用 check句柄的回调. check句柄本质上是Prepare句柄的对应句柄
  10. 调用close的回调. 如果一个句柄调用了 uv_close() 关闭句柄,它将调用close回调
  11. 循环使用 UV_RUN_ONCE (看名字就知道是执行一次迭代)运行时会存在执行前面过程的特殊情况。 有可能在 I/O 阻塞后没有触发任何 I/O 回调,但是已经过了一段时间,所以可能有到点的计时器,这些计时器会调用它们的回调。
  12. 迭代结束. 如果循环以 UV_RUN_NOWAIT 或 UV_RUN_ONCE 模式运行,则迭代结束并且 uv_run() 将返回。 如果循环使用 UV_RUN_DEFAULT 运行,且loop依然是alive的,它将从头开始继续,否则它也会结束。

File I/O

与网络 I/O 不同,libuv 没有可以依赖的特定于平台的文件 I/O 原语,因此当前的方法是在线程池中运行阻塞文件 I/O 操作。

有关跨平台文件 I/O 格局的详细说明,请查看这篇文章

libuv 当前使用一个全局线程池,所有循环都可以在其中排队工作。 当前在此池上运行 3 种类型的操作:

  • File system operations
  • DNS functions (getaddrinfo and getnameinfo)
  • User specified code via uv_queue_work()

Warning:

有关详细信息,请参阅线程池工作调度部分,但请记住线程池大小非常有限。

一条评论

  1. Pingback:libuv : User guide » Utilities – Kingdo Station

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注