Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Node.js 子线程 crash 问题的排查

Node.js 子线程 crash 问题的排查

作者头像
theanarkh
发布于 2022-07-01 07:14:25
发布于 2022-07-01 07:14:25
76300
代码可运行
举报
文章被收录于专栏:原创分享原创分享
运行总次数:0
代码可运行

前言:昨天碰到了一个 worker_threads crash 的问题,最终经过阅读源码和调试找到了具体原因。不得不说,阅读源码是解决问题的非常有效的方法。

代码例子如下。

index.js

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const addon = require.resolve('./build/Release/addon.node');// this makes addon not be unloadedrequire(addon);const { Worker } = require('worker_threads');new Worker(`require('${addon}').start();`, {eval: true});

event_loop.cc

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include "event_loop.h"void on_close(uv_handle_t *handle){    delete handle;}
void cleanup(void* data){    uv_close((uv_handle_t *)data, on_close);}
void Start(const Napi::CallbackInfo &args){    Napi::Env env = args.Env();
    uv_loop_t *loop;
    v8::Isolate* isolate = v8::Isolate::GetCurrent();
    napi_get_uv_event_loop(env, &loop);
    uv_prepare_t* prepare_handle = new uv_prepare_t;
    uv_prepare_init(loop, prepare_handle);
    uv_unref((uv_handle_t *)prepare_handle);
    uv_prepare_start(prepare_handle, [](uv_prepare_t *handle) {});
    node::AddEnvironmentCleanupHook(isolate, cleanup, prepare_handle);}
Napi::Object Initialize(Napi::Env env, Napi::Object exports){    exports.Set(Napi::String::New(env, "start"), Napi::Function::New(env, Start));
    return exports;}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Initialize)

总的来说就是我需要在 worker_threads 里使用 addon,然后在子线程退出时发生了 segmentation fault,但是在主线程里是没问题的(完整代码可参考 https://github.com/theanarkh/test_worker_thread)。首先分析下上面代码的过程,当在 JS 层执行 start 的时候,就会往 loop 里面插入一个任务,并通过 AddEnvironmentCleanupHook 注册了一个回调,这个回调在线程退出时会被执行,执行完 start 后线程就退出了,所以这时候 AddEnvironmentCleanupHook 的回调 cleanup 会被执行,cleanup 里调用 uv_close 关闭 handle,接着在线程真正退出时会执行一次 uv_run 处理 uv_close 的回调,从而释放内存。问题发生在执行 uv_close 的回调时出现了 crash。通过调试发现调用 uv_close 时传入的回调函数地址是 A,但是最终执行时地址变成了 B,而 B 是一个非法地址,从而导致了 crash。出现这个问题时,我就开始调试,尝试找出哪里修改了这个地址,但是无果,最终靠灵光一现,想到了动态链接库被卸载的问题,然后通过打断点发现果然如此。

下面通过 Node.js 的源码来分析这个问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
WorkerThreadData data(this);
  {
    Locker locker(isolate_);
    Isolate::Scope isolate_scope(isolate_);
    SealHandleScope outer_seal(isolate_);

    DeleteFnPtr<Environment, FreeEnvironment> env_;
    // 离开作用域时执行 env_.reset();
    auto cleanup_env = OnScopeLeave([&]() {
      isolate_->CancelTerminateExecution();
      env_.reset();
    });
    // 初始化子线程
    {
      HandleScope handle_scope(isolate_);
      Local<Context> context;
      {
        TryCatch try_catch(isolate_);
        context = NewContext(isolate_);
      }

      Context::Scope context_scope(context);
      {
        env_.reset(CreateEnvironment(
            data.isolate_data_.get(),
            context,
            std::move(argv_),
            std::move(exec_argv_),
            static_cast<EnvironmentFlags::Flags>(environment_flags_),
            thread_id_,
            std::move(inspector_parent_handle_)));
      }
      {
        Mutex::ScopedLock lock(mutex_);
        if (stopped_) return;
        this->env_ = env_.get();
      }

      {
        if (LoadEnvironment(env_.get(), StartExecutionCallback{}).IsEmpty())
          return;
      }
    }
    // 进入子线程事件循环
    {
      Maybe<int> exit_code = SpinEventLoop(env_.get());
      Mutex::ScopedLock lock(mutex_);
      if (exit_code_ == 0 && exit_code.IsJust()) {
        exit_code_ = exit_code.FromJust();
      }
    }
  }

上面是子线程执行时的核心逻辑,当子线程退出时,OnScopeLeave 的第一个函数参数会被执行,从而执行 env_.reset(),接着执行 FreeEnvironment。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void FreeEnvironment(Environment* env) {
  Isolate* isolate = env->isolate();
  Isolate::DisallowJavascriptExecutionScope disallow_js(isolate,
      Isolate::DisallowJavascriptExecutionScope::THROW_ON_FAILURE);
  {
    HandleScope handle_scope(isolate);  // For env->context().
    Context::Scope context_scope(env->context());
    SealHandleScope seal_handle_scope(isolate);

    env->set_stopping(true);
    env->stop_sub_worker_contexts();
    // 执行 AddEnvironmentCleanupHook 回调
    env->RunCleanup();
    RunAtExit(env);
  }

  MultiIsolatePlatform* platform = env->isolate_data()->platform();
  if (platform != nullptr)
    platform->DrainTasks(isolate);
  // 删除 env 对象
  delete env;}

FreeEnvironment 首先通过来 RunCleanup 执行通过 AddEnvironmentCleanupHook 注册的回调,回到开始的代码就是执行 uv_close 往 loop 里插入一个回调。接着 FreeEnvironment 删除了 env 对象,接下来看 env 的析构函数中相关的代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  if (!is_main_thread()) {
    for (binding::DLib& addon : loaded_addons_) {
      addon.Close();
    }
  }

如果当前是子线程,析构函数会调用 addon.Close() 关闭动态链接库,也就是 addon,当 addon 的引用数为 0 就会被卸载。因为只有子线程里用到了 addon 所以 addon 会被卸载。这时候 uv_close 回调函数的地址就被修改了。env 处理完之后,接着是 WorkerThreadData 被析构,WorkerThreadData 析构函数中会再执行一次 uv_run 处理剩下的任务。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
uv_run(&loop_, UV_RUN_ONCE);

所以 uv_close 的回调就会被执行,因为这时候回调函数的地址被修改成非法的了,所以导致了 crash。除了这个问题外,子线程退出前还会检查 loop,如果还有任务没有被关闭也会导致线程 crash。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void CheckedUvLoopClose(uv_loop_t* loop) {
  if (uv_loop_close(loop) == 0) return;

  PrintLibuvHandleInformation(loop, stderr);

  fflush(stderr);
  // Finally, abort.
  CHECK(0 && "uv_loop_close() while having open handles");}

再看 uv_loop_close

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int uv_loop_close(uv_loop_t* loop) {
  QUEUE* q;
  uv_handle_t* h;

  if (uv__has_active_reqs(loop))
    return UV_EBUSY;

  QUEUE_FOREACH(q, &loop->handle_queue) {
    h = QUEUE_DATA(q, uv_handle_t, handle_queue);
    if (!(h->flags & UV_HANDLE_INTERNAL))
      return UV_EBUSY;
  }

  uv__loop_close(loop);

  if (loop == default_loop_ptr)
    default_loop_ptr = NULL;

  return 0;}

总结:这个问题排查了很长的时间,最终靠一个切入点成功找到了问题,并通过源码深入了解了这个过程。源码,是学习一门技术非常重要的资料。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Node.js 内核的幕后英雄 --- 子线程
前言:Node.js 为人所知的是单线程应用,也为人所知的是底层其实利用了多线程。单线程会使得代码实现上变得容易好理解,但是带来好处的同时,也往往会存在一些限制,这些限制导致在 Node.js 内核中,不得不引入其他子线程,最终形成多线程。本文介绍 Node.js 中的这些幕后英雄。
theanarkh
2022/03/30
1.6K0
Node.js 内核的幕后英雄 --- 子线程
nodejs之启动源码解析浅析
int main(int argc, char *argv[]) { #if defined(__linux__) char** envp = environ; while (*envp++ != nullptr) {} Elf_auxv_t* auxv = reinterpret_cast<Elf_auxv_t*>(envp); for (; auxv->a_type != AT_NULL; auxv++) { if (auxv->a_type == AT_SECURE) {
theanarkh
2019/03/19
2.6K0
nodejs之启动源码解析浅析
node.js 中的进程和线程工作原理
进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位,操作系统的其他所有内容都是围绕着进程展开的
ACK
2024/09/18
1970
node.js 中的进程和线程工作原理
nodejs源码分析之线程
我们先分析一下这个代码的意思。因为上面的代码在主线程和子线程都会被执行一遍。所以首先通过isMainThread判断当前是主线程还是子线程。主线程的话,就创建一个子线程,然后监听子线程发过来的消息。子线程的话,首先执行业务相关的代码,还可以监听主线程传过来的消息。下面我们开始分析源码。分析完,会对上面的代码有更多的理解。 首先我们从worker_threads模块开始分析。这是一个c++模块。我们看一下他导出的功能。require("work_threads")的时候就是引用了InitWorker函数导出的功能。
theanarkh
2020/08/10
7650
nodejs源码分析之线程
Nodejs探秘:深入理解单线程实现高并发原理
前言       从Node.js进入我们的视野时,我们所知道的它就由这些关键字组成 事件驱动、非阻塞I/O、高效、轻量,它在官网中也是这么描述自己的: Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.       于是
用户1097444
2022/06/29
2.4K0
Nodejs探秘:深入理解单线程实现高并发原理
Node.js C++ 层的任务管理
我们都知道 Node.js 是基于事件循环来运行的,本质上是一个生产者 / 消费者模型,所以就少不了任务的管理机制,不过本文不是介绍事件循环中的任务管理,而是 C++ 层的任务管理。本文主要介绍 SetImmediate、SetImmediateThreadsafe、RequestInterrupt、AddCleanupHook 这四个 API 产生的任务。时间关系,随便写写,权当笔记。
theanarkh
2022/12/06
1.4K0
Node.js子线程调试和诊断指南
调试、诊断子线程最直接的方式就是像调试、诊断主线程一样,但是无论是动态开启还是静态开启,子线程都不可避免地需要内置一些相关的非业务代码,本文介绍另外一种对子线程代码无侵入的调试方式,另外也介绍一下通过子线程调试主线程的方式。
theanarkh
2021/08/25
1.3K0
Node.js子线程调试和诊断指南
通过N-API使用Libuv线程池
Node.js不适合处理耗时操作是一直存在的问题,为此Node.js提供了三种解决方案。
theanarkh
2021/07/08
9660
Nodejs探秘:深入理解单线程实现高并发原理
从Node.js进入我们的视野时,我们所知道的它就由这些关键字组成 事件驱动、非阻塞I/O、高效、轻量,它在官网中也是这么描述自己的。 Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
IMWeb前端团队
2019/12/03
1.2K0
Nodejs探秘:深入理解单线程实现高并发原理
nodejs事件循环阶段之close
close是nodejs每轮事件循环中最后的一个阶段。我们看看怎么使用。我们知道对于一个handle,他的使用一般是init,start,stop。但是如果我们在stop一个handle之后,还有些事情需要处理怎么办?这时候就可以使用close阶段。close阶段可以用来关闭一个handle,并且执行一个回调。比如用于释放动态申请的内存。close阶段的任务由uv_close产生。
theanarkh
2020/03/16
9740
nodejs源码解析之事件循环
nodejs的的事件循环由libuv的uv_run函数实现。在该函数中执行while循环,然后处理各种阶段(phase)的事件回调。事件循环的处理相当于一个消费者,消费由各业务代码生产的任务。下面看一下代码。
theanarkh
2020/01/15
8310
nodejs源码解析之事件循环
如何实现一个 APM watchdog
Hello,大家好,之前说不打算更新公众号了,后面有时间的话还是会偶尔更新下,记录和分享下一些技术相关的内容,今天分享下如何实现一个 APM watchdog。
theanarkh
2023/10/30
2700
如何实现一个 APM watchdog
libuv线程池和主线程通信原理
代码很简单,就是设置一下async_io_watcher的fd和回调,在epoll_wait返回的时候用到。再看uv__io_start。
theanarkh
2020/01/15
1.5K0
通过源码分析nodejs的进程架构
我们知道nodejs是单进程(单线程)的,但是nodejs也为用户实现了多进程的能力,下面我们看一下nodejs里多进程的架构是怎么样的。 nodejs提供同步和异步创建进程的方式。我们首先看一下异步的方式,nodejs创建进程的方式由很多种。但是归根到底是通过spawn函数。所以我们从这个函数开始,看一下整个流程。
theanarkh
2020/03/31
7100
通过源码分析nodejs的进程架构
libuv源码学习笔记:tcp-echo-server
事件循环是 libuv 功能的核心部分,它负责对 I/O 进行轮询,并基于不同的事件源执行它们的回调函数。libuv 的设计目标之一是为了让异步 I/O 操作变得简单易用,同时保持高性能。
晨星成焰
2024/10/20
2990
libuv源码学习笔记:tcp-echo-server
libuv源码阅读(6)--helloworld
每一种都是一种hanlder类型或者request类型,代表某种资源类型或者请求操作的包装结构体,里面的属性字段是为了支持它可以正常工作的而设置的:
wanyicheng
2021/03/12
8161
nodejs源码解析之udp服务器
我们看到创建一个udp服务器很简单,首先申请一个socket对象,在nodejs中和操作系统中一样,socket是对网络通信的一个抽象,我们可以把他理解成对传输层的抽象,他可以代表tcp也可以代表udp。我们看一下createSocket做了什么。
theanarkh
2020/09/11
1.6K0
Node.js 是如何处理请求的
前言:在服务器软件中,如何处理请求是非常核心的问题。不管是底层架构的设计、IO 模型的选择,还是上层的处理都会影响一个服务器的性能,本文介绍 Node.js 在这方面的内容。
theanarkh
2023/10/04
6310
Node.js 是如何处理请求的
Node子进程async/await方法不正常执行的思考和解决
前段时间,我做了一个node模块node-multi-worker ,希望通过这个模块让node能够脱离单线程的限制,具体的使用可以看一下上面的链接。其思路就是注册任务后,分出子进程,然后在主进程需要执行任务时,向reactor子进程发送命令,而reactor收到命令后分配到worker子进程在执行完成后返回结果到主进程。这篇文章主要是为了跟大家分享一下我在开发过程中,遇到的一个问题,如何解决以及对相关知识的一个挖掘。
全栈程序员站长
2022/06/29
6980
深入nodejs的event-loop
event loop是指由libuv提供的,一种实现非阻塞I/O的机制。具体来讲,因为javascript一门single-threaded编程语言,所以nodejs只能把异步I/O操作的实现(非阻塞I/O的实现结果的就是异步I/O)转交给libuv来做。因为I/O既可能发生在很多不同操作系统上(Unix,Linux,Mac OX,Window),又可以分为很多不同类型的I/O(file I/O, Network I/O, DNS I/O,database I/O等)。所以,对于libuv而言,如果当前系统对某种类型的I/O操作提供相应的异步接口的话,那么libuv就使用这些现成的接口,否则的话就启动一个线程池来自己实现。这就是官方文档所说的:“事件循环使Node.js可以通过将操作转移到系统内核中来执行非阻塞I / O操作(尽管JavaScript是单线程的)”的意思。
coder2028
2022/10/21
7850
相关推荐
Node.js 内核的幕后英雄 --- 子线程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验