本文翻译自:Design overview

Changelog: v1.0.0 2019.08.30

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

库提供的不仅仅是对不同I/O轮询机制上的简单抽象:“句柄”和“流”为套接字和其他实体提供了高级抽象;此外,还提供了跨平台的文件I/O和线程功能。

下图可以看出组成libuv的不同部分和它们所涉及的子系统:

句柄和请求

libuv结合事件循环为用户提供了两个要处理的抽象:句柄和请求。

句柄表示能够在活动时执行某些操作的长生命周期对象。比如:

  1. prepare handle在活动时在每次循环迭代时调用一次回调。
  2. 一个TCP服务器句柄,每当有新连接时,该句柄都会调用它的连接回调。

请求表示(通常)短期的操作。这些操作可以在句柄上执行:比如写请求用于在句柄上写数据;或者也可以是独立的:getaddrinfo请求不需要句柄,它们直接运行在循环上。

I/O循环

I/O(或事件)循环是libuv的核心部分。libuv为所有I/O操作准备所需的一切,并将其绑定到一个线程。只要每个事件运行在不同的线程中,就可以运行多个事件循环。libuv事件循环(或任何其他涉及循环或句柄的API)不是线程安全的,除非另有说明。

事件循环遵循非常常见的单线程异步I/O方法:所有(网络)I/O都在非阻塞套接字上执行,这些套接字使用给定平台上可用的最佳机制进行轮询:Linux上的epoll、OSX上的kqueue和其他BSDs、SunOS上的事件端口和Windows上的IOCP。作为循环迭代的一部分,循环将阻塞已添加到轮询器的套接字上的I/O活动,并触发回调,指示套接字条件(可读、可写挂起),以便句柄可以读、写或执行所需的I/O操作。

为了更好地理解事件循环是如何运行的,下面的图演示了循环迭代的所有阶段:

  1. 循环概念中的“now”得到了更新。事件循环在事件循环开始时缓存当前时间,以减少与时间相关的系统调用的数量。

  2. 如果循环是存活的,则开始迭代,否则循环将立即退出。那么,循环何时被认为是存活的呢?如果循环具有存活的和引用的句柄、活动请求或closing句柄,则认为它是存活的。

  3. 运行到期计时器。所有活动计时器都在循环概念now让其回调都被调用之前调度一段时间。

  4. 调用挂起的回调(pending callbacks)。大多数情况下,所有I/O回调都是在轮询I/O之后立即调用的。然而,在某些情况下,调用这样的回调会被推迟到下一次循环迭代。如果之前的迭代延迟了任何I/O回调,那么它将在此时运行。

  5. 调用空闲句柄回调。虽然名字有点奇怪,但是如果空闲句柄是活动的,那么它们将在每个循环迭代中运行。

  6. 调用Prepare句柄回调。prepare句柄在循环阻塞I/O之前得到回调。

  7. 计算轮询超时。在阻塞I/O之前,事件循环会计算它应该阻塞多长时间。下面是计算超时的规则:

    1. 如果循环使用UV_RUN_NOWAIT标志运行,超时为0。
    2. 如果循环将要停止(调用uv_stop()),超时为0。
    3. 如果没有活动句柄或请求,超时为0。
    4. 如果有任何空闲句柄处于活动状态,超时为0。
    5. 如果有任何句柄等待关闭,超时为0。
    6. 如果以上情况都不匹配,则采用最近计时器的超时,或者如果没有活动计时器,则为无穷大。
  8. 循环阻塞I/O。此时,循环将在上一步计算的持续时间内阻塞I/O。监视读写操作的给定文件描述符的所有I/O相关句柄此时都会调用它们的回调。

  9. 检查调用了句柄回调。检查句柄会在I/O阻塞循环之后立即调用它们的回调。检查句柄本质上是准备句柄的对应物。

  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运行的,那么如果它仍然是活动的,那么它将从1开始继续,否则它也将结束。

重要:libuv使用线程池使异步文件I/O操作成为可能,但是网络I/O总是在单个线程中执行,每个循环的线程都是这样。

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

文件I/O

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

要了解跨平台文件I/O的详细说明,请参阅本文

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

  1. 文件系统操作
  2. DNS函数(getaddrinfo和getnameinfo)
  3. 通过uv_queue_work()指定用户代码

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