【问题标题】:How does the libuv implementation of *non-blockingness* work exactly?*non-blockingness* 的 libuv 实现是如何工作的?
【发布时间】:2020-07-24 13:15:29
【问题描述】:

所以我刚刚发现,就 C 库而言,libuv 是一个相当小的库(与 FFmpeg 相比)。在过去的 6 个小时里,我一直在阅读源代码,以更深入地了解事件循环。但仍然没有看到“非阻塞性”是在哪里实现的。在代码库中调用了一些事件中断信号或诸如此类的东西。

我已经使用 Node.js 超过 8 年,所以我熟悉如何使用异步非阻塞事件循环,但我从未真正研究过实现。

我的问题有两个:

  1. libuv 中发生的“循环”到底在哪里?
  2. 循环的每次迭代中有哪些关键步骤使其非阻塞异步

所以我们从一个 hello world 示例开始。只需要这样:

#include <stdio.h>
#include <stdlib.h>
#include <uv.h>

int main() {
  uv_loop_t *loop = malloc(sizeof(uv_loop_t));
  uv_loop_init(loop); // initialize datastructures.
  uv_run(loop, UV_RUN_DEFAULT); // infinite loop as long as queue is full?
  uv_loop_close(loop);
  free(loop);
  return 0;
}

我一直在探索的关键函数是uv_runuv_loop_init 函数本质上是初始化数据结构,所以我不认为那里有太多的幻想。但真正的魔力似乎发生在uv_run某处。 libuv repo 中的一组高级代码 sn-ps 是 in this gist,显示了 uv_run 函数调用的内容。

基本上它似乎归结为:

while (NOT_STOPPED) {
  uv__update_time(loop)
  uv__run_timers(loop)
  uv__run_pending(loop)
  uv__run_idle(loop)
  uv__run_prepare(loop)
  uv__io_poll(loop, timeout)
  uv__run_check(loop)
  uv__run_closing_handles(loop)
  // ... cleanup
}

这些功能在要点中。

  • uv__run_timers:运行定时器回调?循环使用for (;;) {
  • uv__run_pending:运行定期回调?使用while (!QUEUE_EMPTY(&amp;pq)) { 循环遍历队列。
  • uv__run_idle: 没有源代码
  • uv__run_prepare: 没有源代码
  • uv__io_poll: io 轮询吗? (不能完全说出这意味着什么)。有 2 个循环:while (!QUEUE_EMPTY(&amp;loop-&gt;watcher_queue)) {for (;;) {

然后我们就完成了。程序存在,因为没有“工作”要做。

所以我想在所有这些挖掘之后我已经回答了我的问题的第一部分,并且循环具体在这三个函数中:

  1. uv__run_timers
  2. uv__run_pending
  3. uv__io_poll

但是没有用kqueue 或多线程实现任何东西,并且对文件描述符的处理相对较少,我不太关注代码。这可能也会帮助其他人学习这一点。

那么问题的第二部分是实现非阻塞性的这三个函数的关键步骤是什么?假设这是所有循环存在的地方。

不是 C 专家,for (;;) { 会“阻止”事件循环吗?或者它可以无限期地运行并且以某种方式从操作系统系统事件或类似的东西跳转到代码的其他部分?

所以uv__io_poll 在那个无限循环中调用poll(...)。我不认为是非阻塞的,对吗?这似乎就是它的主要作用。

查看kqueue.c 还有一个uv__io_poll,所以我假设poll 实现是一个后备,并且使用Mac 上的kqueue,这是非阻塞的?

是这样吗?它只是在uv__io_poll 中循环,并且每次迭代都可以添加到队列中,只要队列中有东西它就会运行?我仍然看不出它是如何非阻塞和异步的。

是否可以与此类似地概述一下它是如何异步和非阻塞的,以及查看代码的哪些部分?基本上,我想看看 libuv 中存在“空闲处理器空闲”的位置。在调用我们最初的uv_run 时,处理器在哪里空闲?如果它是免费的,它如何像事件处理程序一样被重新调用? (就像来自鼠标的浏览器事件处理程序,一个中断)。我觉得我在寻找中断但没有看到。

我问这个是因为我想在 C 中实现一个 MVP 事件循环,但只是不明白非阻塞性实际上是如何实现的。橡胶与道路的交汇处。

【问题讨论】:

    标签: c asynchronous io event-loop libuv


    【解决方案1】:

    我认为试图理解 libuv 会妨碍您理解反应器(事件循环)是如何在 C 中实现的,而您需要理解的是这一点,而不是 libuv 背后的确切实现细节。

    (请注意,当我说“在 C 中”时,我真正的意思是“在系统调用接口处或附近,用户空间与内核相遇的地方”。)

    所有不同的后端(select、poll、epoll 等)或多或少都是同一主题的变体。它们阻塞当前进程或线程,直到有工作要做,例如服务计时器、从套接字读取、写入套接字或处理套接字错误。

    当当前进程被阻塞时,它实际上没有得到操作系统调度程序分配给它的任何 CPU 周期。

    理解这些东西 IMO 背后的部分问题是糟糕的术语:异步,JS 领域的同步,它们并没有真正描述这些东西是什么。实际上,在 C 语言中,我们讨论的是非阻塞与阻塞 I/O。

    当我们从阻塞文件描述符中读取时,进程(或线程)被阻塞——阻止运行——直到内核有东西可供读取;当我们写入一个阻塞文件描述符时,进程会被阻塞,直到内核接受整个缓冲区。

    在非阻塞 I/O 中,它完全一样,只是内核不会在无事可做时停止进程运行:相反,当你读或写时,它会告诉你你读了多少或写了多少写(或者如果有错误)。

    select 系统调用(和朋友们)可以防止 C 开发人员不得不一遍又一遍地尝试从非阻塞文件描述符中读取——select() 实际上是一个阻塞系统调用,它在任何情况下都会解除阻塞您正在观看的描述符或计时器已准备就绪。这让开发人员可以围绕 select 构建一个循环,为它报告的任何事件提供服务,例如过期的超时或可以读取的文件描述符。 这是事件循环。

    所以,从本质上讲,在 JS 事件循环的 C 端发生的事情大致是这样的算法:

    while(true) {
      select(open fds, timeout);
      did_the_timeout_expire(run_js_timers());
      for (each error fd)
        run_js_error_handler(fdJSObjects[fd]);
      for (each read-ready fd)
        emit_data_events(fdJSObjects[fd], read_as_much_as_I_can(fd));
      for (each write-ready fd) {
        if (!pendingData(fd))
          break;
        write_as_much_as_I_can(fd);
        pendingData = whatever_was_leftover_that_couldnt_write; 
      }
    }
    

    FWIW - 我实际上已经为基于 select() 的 v8 编写了一个事件循环:真的就是这么简单。

    记住 JS 总是运行到完成也很重要。因此,当您从 C 调用 JS 函数(通过 v8 api)时,您的 C 程序在 JS 代码返回之前不会执行任何操作。

    NodeJS 使用了一些优化,例如在单独的 pthread 中处理挂起的写入,但这些都发生在“C 空间”中,在尝试理解这种模式时你不应该考虑/担心它们,因为它们不相关。

    您可能还误以为在处理异步函数之类的事情时 JS 没有运行完成——但绝对是 100% 的时间——如果你没有跟上这方面的速度,对事件循环和微任务队列做一些阅读。异步函数基本上是一种语法技巧,它们的“完成”涉及返回一个 Promise。

    【讨论】:

      【解决方案2】:

      我刚刚深入研究了libuv 的源代码,起初发现它似乎做了很多设置,并没有太多实际的事件处理。

      不过,看看 src/unix/kqueue.c reveals 一些事件处理的内部机制:

      int uv__io_check_fd(uv_loop_t* loop, int fd) {
        struct kevent ev;
        int rc;
      
        rc = 0;
        EV_SET(&ev, fd, EVFILT_READ, EV_ADD, 0, 0, 0);
        if (kevent(loop->backend_fd, &ev, 1, NULL, 0, NULL))
          rc = UV__ERR(errno);
      
        EV_SET(&ev, fd, EVFILT_READ, EV_DELETE, 0, 0, 0);
        if (rc == 0)
          if (kevent(loop->backend_fd, &ev, 1, NULL, 0, NULL))
            abort();
      
        return rc;
      }
      

      文件描述符轮询在这里完成,使用EV_SET“设置”事件(类似于在使用select()检查之前使用FD_SET的方式),并通过kevent处理程序完成处理。

      这是特定于kqueue 风格的事件(主要用于BSD-likes a la MacOS),对于不同的Unices 还有许多其他实现,但它们都使用相同的函数名来进行非阻塞IO 检查。使用epoll 的另一个实现请参见here

      回答您的问题:

      1) libuv 中的“循环”究竟发生在哪里?

      QUEUE 数据结构用于存储和处理事件。此队列由您注册侦听的特定于平台和 IO 的事件类型填充。在内部,它使用了一个巧妙的链表,仅使用两个 void * 指针 (see here) 组成的数组:

      typedef void *QUEUE[2];

      我不打算详细介绍这个列表,您只需要知道它实现了一个类似队列的结构来添加和弹出元素。

      一旦队列中有正在生成数据的文件描述符,前面提到的异步 I/O 代码就会获取它。 uv_loop_t 结构中的 backend_fd 是每种 I/O 类型的数据生成器。

      2) 循环的每次迭代中有哪些关键步骤使其成为非阻塞和异步的

      libuv 本质上是这里真正主力的包装器(带有一个很好的 API),即kqueue, epoll, select 等。要完全回答这个问题,您需要在内核级文件描述符方面有相当的背景知识实施,根据这个问题,我不确定这是否是您想要的。

      简短的回答是,底层操作系统都具有用于非阻塞(因此是异步)I/O 的内置设施。我认为每个系统的工作原理都超出了这个答案的范围,但我会为好奇的人留下一些阅读:

      https://www.quora.com/Network-Programming-How-is-select-implemented?share=1

      【讨论】:

        【解决方案3】:

        首先要记住的是,必须使用 libuv 的 API 将工作添加到 libuv 的队列中;不能只加载 libuv,启动其主循环,然后编写一些 I/O 并获得异步 I/O。

        libuv 维护的队列是通过循环来管理的。 uv__run_timers 中的无限循环实际上并不是无限的。请注意,第一次检查验证是否存在最快到期的计时器(假设列表为空,则为 NULL),如果不存在,则中断循环并返回函数。如果当前(最快到期的)计时器尚未到期,则下一个检查会中断循环。如果这些条件都没有中断循环,则代码继续:它重新启动计时器,调用其超时处理程序,然后再次循环以检查更多计时器。大多数情况下,当这段代码运行时,它会中断循环并退出,允许其他循环运行。

        使所有这些非阻塞的原因是调用者/用户遵循 libuv 的准则和 API:将您的工作添加到队列中,并允许 libuv 在这些队列上执行其工作。处理密集型工作可能会阻止这些循环和其他工作运行,因此将工作分成块很重要。

        【讨论】:

          【解决方案4】:

          btw, uv__run_idle, uv__run_check, uv__run_prepare 的源代码定义在 src/unix/loop-watcher.c

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-06-10
            • 2012-12-31
            • 1970-01-01
            • 2020-07-31
            • 2014-12-18
            • 2013-07-04
            • 2018-07-21
            • 2020-11-28
            相关资源
            最近更新 更多