Lan*_*ard 7 c io asynchronous event-loop libuv
所以我刚刚发现就 C 库而言,libuv是一个相当小的库(与 FFmpeg 相比)。在过去的 6 个小时里,我通读了源代码,以便更深入地了解事件循环。但仍然没有看到“非阻塞”在哪里实现。在代码库中调用某些事件中断信号或诸如此类的东西。
我已经使用 Node.js 超过 8 年了,所以我熟悉如何使用异步非阻塞事件循环,但我从未真正研究过它的实现。
我的问题是双重的:
因此,我们从一个 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;
}
Run Code Online (Sandbox Code Playgroud)
我一直在探索的关键功能是uv_run. 该uv_loop_init函数本质上是初始化数据结构,所以我不认为那里有太多花哨的东西。但真正的魔力似乎有发生uv_run,地方。来自 libuv 存储库的一组高级代码片段在此 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
}
Run Code Online (Sandbox Code Playgroud)
这些功能在要点中。
uv__run_timers: 运行定时器回调?循环for (;;) {。uv__run_pending: 运行定期回调?循环遍历队列while (!QUEUE_EMPTY(&pq)) {。uv__run_idle:没有源代码uv__run_prepare:没有源代码uv__io_poll: io 投票吗?(不能完全说出这意味着什么)。有 2 个循环:while (!QUEUE_EMPTY(&loop->watcher_queue)) {, 和for (;;) {,然后我们就完成了。程序存在,因为没有“工作”要做。
所以我想我已经在所有这些挖掘之后回答了我问题的第一部分,并且循环特别是在这 3 个函数中:
uv__run_timersuv__run_pendinguv__io_poll但是没有使用kqueue或多线程实现任何东西并且对文件描述符的处理相对较少,我并没有完全遵循代码。这也可能会帮助其他人学习这一点。
那么问题的第二部分是这三个函数中实现非阻塞的关键步骤是什么?假设这是所有循环存在的地方。
不是 C 专家,是否for (;;) {“阻塞”了事件循环?或者可以无限期地运行并且以某种方式从操作系统系统事件或类似的东西跳转到代码的其他部分?
所以在那个无限循环中uv__io_poll调用poll(...)。我不认为是非阻塞的,对吗?这似乎就是它主要做的所有事情。
看看kqueue.c还有一个uv__io_poll,所以我假设poll实现是一个后备,kqueue在 Mac 上使用,这是非阻塞的?
所以是这样吗?它是否只是循环,uv__io_poll并且每次迭代都可以添加到队列中,并且只要队列中有东西它就会运行?我仍然没有看到它是如何非阻塞和异步的。
是否可以概述与此类似的异步和非阻塞方式,以及要查看代码的哪些部分?基本上,我想看看 libuv 中存在“空闲处理器空闲”的位置。在调用我们的初始时处理器在哪里空闲uv_run?如果它是免费的,它如何像事件处理程序一样被重新调用?(就像来自鼠标的浏览器事件处理程序,一个中断)。我觉得我正在寻找一个中断但没有看到一个。
我问这个是因为我想在 C 中实现一个 MVP 事件循环,但只是不明白非阻塞实际上是如何实现的。橡胶与道路相遇的地方。
我刚刚深入研究了libuv的源代码,起初发现它似乎做了很多设置,但没有太多实际的事件处理。
尽管如此,研究src/unix/kqueue.c 揭示了事件处理的一些内部机制:
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;
}
Run Code Online (Sandbox Code Playgroud)
文件描述符轮询是在此处完成的,“设置”事件EV_SET(类似于FD_SET在检查之前使用的方式select()),并且处理是通过kevent处理程序完成的。
这是特定于kqueue样式事件的(主要用于 BSD-likes a la MacOS),并且对于不同的 Unice 有许多其他实现,但它们都使用相同的函数名称来执行非阻塞 IO 检查。请参阅此处了解使用 的另一个实现epoll。
回答您的问题:
1)libuv中“循环”到底发生在哪里?
数据QUEUE结构用于存储和处理事件。该队列由您注册侦听的特定于平台和 IO 的事件类型填充。在内部,它使用一个巧妙的链表,仅使用两个void *指针的数组(请参见此处):
typedef void *QUEUE[2];
我不打算详细介绍这个列表,您只需要知道它实现了一个类似队列的结构来添加和弹出元素。
一旦队列中有正在生成数据的文件描述符,前面提到的异步 I/O 代码就会拾取它。该结构backend_fd内部uv_loop_t是每种类型 I/O 的数据生成器。
2)循环的每次迭代中使其成为非阻塞和异步的关键步骤是什么?
libuv本质上是一个围绕真正的主力的包装器(带有一个很好的API),即kqueue, epoll, select,等等。要完全回答这个问题,您需要在内核级文件描述符实现方面有相当多的背景知识,我不确定是否根据问题,这就是你想要的。
简而言之,底层操作系统都具有用于非阻塞(因此异步)I/O 的内置设施。我认为每个系统的工作原理有点超出了这个答案的范围,但我会为好奇的人留下一些阅读内容:
https://www.quora.com/Network-Programming-How-is-select-implemented?share=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;
}
}
Run Code Online (Sandbox Code Playgroud)
FWIW - 我实际上已经基于 select() 为 v8 编写了一个事件循环:这真的很简单。
同样重要的是要记住 JS 总是运行到完成。因此,当您从 C 调用 JS 函数(通过 v8 api)时,您的 C 程序在 JS 代码返回之前不会执行任何操作。
NodeJS 使用了一些优化,比如在单独的 pthread 中处理挂起的写入,但这些都发生在“C 空间”中,在尝试理解这种模式时,你不应该考虑/担心它们,因为它们不相关。
您可能还会误以为在处理异步函数之类的事情时 JS 不会运行完成——但它绝对是,100% 的时间——如果你没有跟上这个速度,请做一些阅读关于事件循环和微任务队列。异步函数基本上是一种语法技巧,它们的“完成”涉及返回一个 Promise。