高性能异步 I/O 模型库 libuv 设计思路概述

       使用libevent这个库很长时间了,libevent虽然比较成熟,但由于这个库考虑到多线程的问题,里面的线程同步锁太多性能不是很好,同时问题也发现不少,尤其是在Linux下,时常莫名崩溃,很难找到原因。好在libuv现在已经很成熟了,经过使用发现性能非常优秀,有必要扩大使用。

       libuv原来是为了Node.js而写的一个跨平台支撑库。它是围绕事件驱动的异步I/O模型设计的。

      该库提供的不仅仅是针对不同I/O轮询机制的简单抽象:“handles”(句柄)和“streams”(流)为套接字和其他实体提供了高级抽象;还提供了跨平台文件I/O和多线程功能,还包括一些其它东西。

    如下的图表说明组成libuv的不同部分以及它们与哪个子系统相关:

  高性能异步 I/O 模型库 libuv 设计思路概述_第1张图片

handles(句柄)和requests(请求)

libuv给用户提供了与event loop(事件循环)结合使用的两种抽象对象:handles(句柄) 与 requests(请求)。

handles 表示在活跃时能够执行某些操作的长寿命对象。例如:

 1. 处于激活状态的prepare handle 在每次循环迭代中获得一次回调。
 2. TCP 服务handle在每次新的连接到达时获得一个connection (连接)回调。

Requests 代表(通常)短期操作。这些操作可能是在一个handle对象上执行的:如write requests(写请求)用于在一个handle(句柄)上写数据;或者是独立的,如 getaddrinfo requests(请求)无须句柄可以直接在事件循环上运行。

 

       事件循环是 libuv 的核心部分。它为所有的 I/O 操作建立了上下文,并且执行于一个单线程中。你可以在多个不同的线程中运行多个事件循环。除非另有说明,不然 libuv 的事件循环(以及其他循环或句柄提供的 API)并不是线程安全的

        事件循环遵循着普遍的单线程异步 I/O 行为:所有的(网络)I/O 体现在非阻塞的 socket 上,对于不同的平台,libuv 会选取最佳的轮询机制:Linux 上为 epoll ,OSX 和其他 BSD 上为 kqueue ,SunOS 上为 event ports , Windows 上则为 IOCP 。作为循环迭代的一部分,事件循环会阻塞并等待被添加的 socket 上 I/O 活动的发生。然后根据当前的 socket 情况(可读,可写,挂起)触发相应的回调函数。所以,一个句柄是可以执行读操作,写操作或其他 I/O 行为。

为了能更好的理解事件循环是如何工作的,下图展示了事件循环一次迭代的所有阶段:

高性能异步 I/O 模型库 libuv 设计思路概述_第2张图片

  1. 事件循环中的“现在时间(now)”被更新。事件循环会在一次循环迭代开始的时候缓存下当时的时间,用于减少与时间相关的系统调用次数。

  2. 如果事件循环仍是存活(alive)的,那么迭代就会开始,否则循环会立刻退出。如果一个循环内包含激活的可引用句柄,激活的请求或正在关闭的句柄,那么则认为该循环是存活的。

  3. 执行超时定时器(due timers)。所有在循环的“现在时间”之前超时的定时器都将在这个时候得到执行。

  4. 执行等待中回调(pending callbacks)。正常情况下,所有的 I/O 回调都会在轮询 I/O 后立刻被调用。但是有些情况下,回调可能会被推迟至下一次循环迭代中再执行。任何上一次循环中被推迟的回调,都将在这个时候得到执行。

  5. 执行闲置句柄回调(idle handle callbacks)。尽管它有个不怎么好听的名字,但只要这些闲置句柄是激活的,那么在每次循环迭代中它们都会执行。

  6. 执行预备回调(prepare handle)。预备回调会在循环为 I/O 阻塞前被调用。

  7. 开始计算轮询超时(poll timeout)。在为 I/O 阻塞前,事件循环会计算它即将会阻塞多长时间。以下为计算该超时的规则:

    • 如果循环带着 UV_RUN_NOWAIT 标识执行,那么超时将会是 0 。

    • 如果循环即将停止(uv_stop() 已在之前被调用),那么超时将会是 0 。

    • 如果循环内没有激活的句柄和请求,那么超时将会是 0 。

    • 如果循环内有激活的闲置句柄,那么超时将会是 0 。

    • 如果有正在等待被关闭的句柄,那么超时将会是 0 。

    • 如果不符合以上所有,那么该超时将会是循环内所有定时器中最早的一个超时时间,如果没有任何一个激活的定时器,那么超时将会是无限长(infinity)。

  8. 事件循环为 I/O 阻塞。此时事件循环将会为 I/O 阻塞,持续时间为上一步中计算所得的超时时间。所有与 I/O 相关的句柄都将会监视一个指定的文件描述符,等待一个其上的读或写操作来激活它们的回调。

  9. 执行检查句柄回调(check handle callbacks)。在事件循环为 I/O 阻塞结束后,检查句柄的回调将会立刻执行。检查句柄本质上是预备句柄的对应物(counterpart)。

  10. 执行关闭回调(close callbacks)。如果一个句柄通过调用 uv_close() 被关闭,那么这将会调用关闭回调。

  11. 尽管在为 I/O 阻塞后可能并没有 I/O 回调被触发,但是仍有可能这时已经有一些定时器已经超时。若事件循环是以 UV_RUN_ONCE 标识执行,那么在这时这些超时的定时器的回调将会在此时得到执行。

  12. 迭代结束。如果循环以 UV_RUN_NOWAIT 或 UV_RUN_ONCE 标识执行,迭代便会结束,并且 uv_run() 将会返回。如果循环以 UV_RUN_DEFAULT 标识执行,那么如果若它还是存活的,它就会开始下一次迭代,否则结束。

重要:虽然 libuv 的异步文件 I/O 操作是通过线程池实现的,但是网络 I/O 总是在单线程中执行的。

与网络 I/O 不同,并不存在 libuv 可以依靠的各特定平台下的文件 I/O 基础函数,所以目前的实现是在线程中执行阻塞的文件 I/O 操作来模拟异步。

更多关于跨平台异步文件 I/O 操作的内容,可参阅this post.

libuv 目前使用了一个全局的线程池,所有的循环都可以往其中加入任务。目前有三种操作会在这个线程池中执行:

  • 文件系统操作

  • DNS 函数(getaddrinfo 和 getnameinfo)

  • 通过 uv_queue_work() 添加的用户代码

注意:更多关于 libuv 线程池的信息请参阅此文。请牢记线程池的大小是有限的。

 

你可能感兴趣的:(Linux,C++)