首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >通过源码分析nodejs的进程架构

通过源码分析nodejs的进程架构

作者头像
theanarkh
发布于 2020-03-31 09:54:40
发布于 2020-03-31 09:54:40
7710
举报
文章被收录于专栏:原创分享原创分享

我们知道nodejs是单进程(单线程)的,但是nodejs也为用户实现了多进程的能力,下面我们看一下nodejs里多进程的架构是怎么样的。 nodejs提供同步和异步创建进程的方式。我们首先看一下异步的方式,nodejs创建进程的方式由很多种。但是归根到底是通过spawn函数。所以我们从这个函数开始,看一下整个流程。

代码语言:javascript
AI代码解释
复制
var spawn = exports.spawn = function(/*file, args, options*/) {

  var opts = normalizeSpawnArguments.apply(null, arguments);
  var options = opts.options;
  var child = new ChildProcess();

  debug('spawn', opts.args, options);

  child.spawn({
    file: opts.file,
    args: opts.args,
    cwd: options.cwd,
    windowsHide: !!options.windowsHide,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments,
    detached: !!options.detached,
    envPairs: opts.envPairs,
    stdio: options.stdio,
    uid: options.uid,
    gid: options.gid
  });

  return child;
};

我们看到spawn函数只是对ChildProcess函数的封装。然后调用他的spawn函数(只列出核心代码)。

代码语言:javascript
AI代码解释
复制
const { Process } = process.binding('process_wrap');

function ChildProcess() {
  EventEmitter.call(this);
  this._handle = new Process();
}

ChildProcess.prototype.spawn = function(options) {
    this._handle.spawn(options);
}

ChildProcess也是对Process的封装。Process是js层和c++层的桥梁,我们找到他对应的c++模块。

代码语言:javascript
AI代码解释
复制
NODE_BUILTIN_MODULE_CONTEXT_AWARE(process_wrap, node::ProcessWrap::Initialize)

即在js层调用process.binding('process_wrap')的时候,拿到的是node::ProcessWrap::Initialize导出的对象。

代码语言:javascript
AI代码解释
复制
  static void Initialize(Local<Object> target,
                         Local<Value> unused,
                         Local<Context> context
  ) {
    Environment* env = Environment::GetCurrent(context);
    // 定义一个构造函数,值是New
    Local<FunctionTemplate> constructor = env->NewFunctionTemplate(New);
    constructor->InstanceTemplate()->SetInternalFieldCount(1);
    //  拿到一个字符串
    Local<String> processString =
        FIXED_ONE_BYTE_STRING(env->isolate(), "Process");
    constructor->SetClassName(processString);

    AsyncWrap::AddWrapMethods(env, constructor);
    // 设置这个构造函数的原型方法
    env->SetProtoMethod(constructor, "close", HandleWrap::Close);

    env->SetProtoMethod(constructor, "spawn", Spawn);
    env->SetProtoMethod(constructor, "kill", Kill);
    ...
    /*
        类似js里的module.exports = {Process: New}
        js层new Process的时候会相对于执行new New
    */
    target->Set(processString, constructor->GetFunction());
  }

上面的代码翻译成js大概如下。

代码语言:javascript
AI代码解释
复制
function New() {}
New.prototype = {
    spawn: Spawn,
    kill: Kill,
    close: Close
    ...
}
module.exports = {Process: New}

所以new Process的时候,执行的是new New。

代码语言:javascript
AI代码解释
复制
static void New(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   new ProcessWrap(env, args.This());
}

 ProcessWrap(Environment* env, Local<Object> object)
     : HandleWrap(
           env,
           object,
           reinterpret_cast<uv_handle_t*>(&process_),
           AsyncWrap::PROVIDER_PROCESSWRAP
    ) 
{
}

我们看到new New就是new ProcessWrap,但是New函数没有返回一个值。继续往下看。

代码语言:javascript
AI代码解释
复制
HandleWrap::HandleWrap(...) {
  Wrap(object, this);
}

void Wrap(v8::Local<v8::Object> object, TypeName* pointer) {
  object->SetAlignedPointerInInternalField(0, pointer);
}

v8的套路有点复杂,大致就是在FunctionCallbackInfo对象里保存了ProcessWrap类的对象。后续调用的时候会取出来。然后给js返回一个对象。接着

代码语言:javascript
AI代码解释
复制
this._handle.spawn(options);

这时候会执行

代码语言:javascript
AI代码解释
复制
  static void Spawn(const FunctionCallbackInfo<Value>& args) {
    Environment* env = Environment::GetCurrent(args);
    Local<Context> context = env->context();
    ProcessWrap* wrap;
    /*
        取出刚才的ProcessWrap对象
        wrap = args->GetAlignedPointerFromInternalField(0);
    */
    ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());

    int err = uv_spawn(env->event_loop(), &wrap->process_, &options);

    args.GetReturnValue().Set(err);
  }

接着我们通过uv_spawn来到了c语言层。uv_spawn总的来说做了下面几个事情。 1 主进程注册SIGCHLD信号,处理函数为uv__chld。SIGCHLD信号是子进程退出时发出的。 2 处理进程间通信、标准输入、输出。 3 fork出子进程 4 在uv_process_t结构图中保存子进程信息,uv_process_t是c++层和c层的联系。 5 把uv_process_t插入libuv事件循环的process_handles队列 6 主进程和子进程各自运行。 整个流程下来,大致形成如图所示的架构。

在这里插入图片描述 当进程退出的时候。nodejs主进程会收到SIGCHLD信号。然后执行uv__chld。该函数遍历libuv进程队列中的节点,通过waitpid判断该节点对应的进程是否已经退出后,从而收集已退出的节点,然后移出libuv队列,最后执行已退出进程的回调。

代码语言:javascript
AI代码解释
复制
static void uv__chld(uv_signal_t* handle, int signum) {
  uv_process_t* process;
  uv_loop_t* loop;
  int exit_status;
  int term_signal;
  int status;
  pid_t pid;
  QUEUE pending;
  QUEUE* q;
  QUEUE* h;
  // 保存进程(已退出的状态)的队列
  QUEUE_INIT(&pending);
  loop = handle->loop;

  h = &loop->process_handles;
  q = QUEUE_HEAD(h);
  //  收集已退出的进程
  while (q != h) {
    process = QUEUE_DATA(q, uv_process_t, queue);
    q = QUEUE_NEXT(q);

    do
      // WNOHANG非阻塞等待子进程退出,其实就是看哪个子进程退出了,没有的话就直接返回,而不是阻塞 
      pid = waitpid(process->pid, &status, WNOHANG);
    while (pid == -1 && errno == EINTR);

    if (pid == 0)
      continue;
    // 进程退出了,保存退出状态,移出队列,插入peding队列,等待处理
    process->status = status;
    QUEUE_REMOVE(&process->queue);
    QUEUE_INSERT_TAIL(&pending, &process->queue);
  }

  h = &pending;
  q = QUEUE_HEAD(h);
  // 是否有退出的进程
  while (q != h) {
    process = QUEUE_DATA(q, uv_process_t, queue);
    q = QUEUE_NEXT(q);
    QUEUE_REMOVE(&process->queue);
    QUEUE_INIT(&process->queue);
    uv__handle_stop(process);

    if (process->exit_cb == NULL)
      continue;

    exit_status = 0;
    // 获取退出信息,执行上传回调
    if (WIFEXITED(process->status))
      exit_status = WEXITSTATUS(process->status);

    term_signal = 0;
    if (WIFSIGNALED(process->status))
      term_signal = WTERMSIG(process->status);

    process->exit_cb(process, exit_status, term_signal);
  }
}

这就是nodejs中进程的整个生命周期。 接下来看看如何以同步的方式创建进程。入口函数是spawnSync。对应的c++模块是spawn_sync。过程就不详细说明了,直接看核心代码。

代码语言:javascript
AI代码解释
复制
void SyncProcessRunner::TryInitializeAndRunLoop(Local<Value> options) {
  int r;

  uv_loop_ = new uv_loop_t;
 // ExitCallback会把子进程的结构体从libuv中移除
  uv_process_options_.exit_cb = ExitCallback;
  r = uv_spawn(uv_loop_, &uv_process_, &uv_process_options_);
  r = uv_run(uv_loop_, UV_RUN_DEFAULT);
}

我们看到,对于同步创建进程,nodejs没有使用waitpid这种方式阻塞自己,从而等待子进程退出。而是重新开启了一个事件循环。我们知道uv_run是一个死循环,所以这时候,nodejs主进程会阻塞在上面的uv_run。直到子进程退出,uv_run才会退出循环,从而再次回到nodejs原来的事件循环。

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
node.js 中的进程和线程工作原理
进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位,操作系统的其他所有内容都是围绕着进程展开的
ACK
2024/09/18
4150
node.js 中的进程和线程工作原理
libuv源码阅读(22)--spawn
总结:父进程fork出子进程去执行指定的文件或者应用,双方根据参数设置的共享fd来通信,这个示例比较简单,父进程只需要等待结束捕获信号就可以了。
wanyicheng
2021/03/14
2.7K0
nodejs之setImmediate源码分析
从上面的代码中我们知道,调用setImmediate函数后,nodejs会把回调和参数存在一个队列里。等待回调。然后处理队列里的每个节点。下面我们看一下处理函数的代码。
theanarkh
2019/03/19
9990
nodejs源码分析之c++层的通用逻辑
我们知道nodejs分为js、c++、c三层,本文以tcp_wrap.cc为例子分析c++层实现的一些通用逻辑。nodejs的js和c++通信原理q.com/s?__biz=MzUyNDE2OTAwN
theanarkh
2020/07/22
7810
nodejs源码分析之线程
我们先分析一下这个代码的意思。因为上面的代码在主线程和子线程都会被执行一遍。所以首先通过isMainThread判断当前是主线程还是子线程。主线程的话,就创建一个子线程,然后监听子线程发过来的消息。子线程的话,首先执行业务相关的代码,还可以监听主线程传过来的消息。下面我们开始分析源码。分析完,会对上面的代码有更多的理解。 首先我们从worker_threads模块开始分析。这是一个c++模块。我们看一下他导出的功能。require("work_threads")的时候就是引用了InitWorker函数导出的功能。
theanarkh
2020/08/10
8030
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.
IMWeb前端团队
2019/12/03
1.3K0
Nodejs探秘:深入理解单线程实现高并发原理
nodejs源码解析之udp服务器
我们看到创建一个udp服务器很简单,首先申请一个socket对象,在nodejs中和操作系统中一样,socket是对网络通信的一个抽象,我们可以把他理解成对传输层的抽象,他可以代表tcp也可以代表udp。我们看一下createSocket做了什么。
theanarkh
2020/09/11
1.7K0
nodejs 14.0.0源码分析之setImmediate
setImmediate的代码比较简单,新建一个Immediate。我们看一下Immediate的类。
theanarkh
2020/02/25
6100
nodejs的http.createServer过程解析
发现_http_server.js也没有太多逻辑,继续看lib/net.js下的代码。
theanarkh
2019/03/15
1.8K0
nodejs的http.createServer过程解析
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.7K0
nodejs之启动源码解析浅析
nodejs源码分析第十九章 -- udp模块
udp不是面向连接的协议,所以使用上会比tcp简单,但是作为传输层的协议,udp虽然没有tcp那么复杂,但是他和tcp一样,使用四元组来标记通信的双方(单播的情况下)。我们看看udp作为服务器和客户端的时候的流程。
theanarkh
2020/09/27
3.1K0
nodejs源码分析第十九章 -- udp模块
深入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
8560
nodejs的错误处理
C++层直接调用Libuv的uv_tcp_connect,并且设置回调是AfterConnect。接着我们看libuv的实现。
theanarkh
2021/04/22
1.8K0
nodejs源码分析之connect
今天我们来分析connect函数。connect是发起tcp连接的api。本质上是对底层tcp协议connect函数的封装。我们看一下nodejs里做了什么事情。我们首先看一下connect函数的入口定义。
theanarkh
2020/07/27
9090
在nodejs中创建child process
nodejs的main event loop是单线程的,nodejs本身也维护着Worker Pool用来处理一些耗时的操作,我们还可以通过使用nodejs提供的worker_threads来手动创建新的线程来执行自己的任务。
程序那些事
2021/01/28
5.1K0
nodejs是如何处理tcp连接的
前几天和一个小伙伴交流了一下nodejs中epoll和处理请求的一些知识,今天简单来聊一下nodejs处理请求的逻辑。我们从listen函数开始。
theanarkh
2021/03/15
1.2K0
Nodejs进程间通信
一.场景 Node运行在单线程下,但这并不意味着无法利用多核/多机下多进程的优势 事实上,Node最初从设计上就考虑了分布式网络场景: Node is a single-threaded, single-process system which enforces shared-nothing design with OS process boundaries. It has rather good libraries for networking. I believe this to be a basis
ayqy贾杰
2019/06/12
3.3K0
通过源码分析nodejs线程架构
nodejs支持了进程之后,又支持了线程。类似浏览器端的web worker。因为nodejs是单线程的,但是底层又实现了一个线程池,接着实现了进程,又实现了线程。一下变得混乱起来,我们要了解这些功能的实现原理,才能更好地使用他。上篇大致分析了进程的原理,这一篇来讲一下线程的原理。只有了解线程的实现,才能知道什么时候应该用线程,为什么可以用线程。 线程的实现也非常复杂。虽然底层只是对线程库的封装,但是把它和nodejs原本的架构结合起来似乎就变得麻烦起来。下面开始分析创建线程的过程。分析线程实现之前,我们先看一下线程通信的实现,因为线程实现中会用到。通俗来说,他的实现类似一个管道。
theanarkh
2020/03/31
6710
通过源码分析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.6K0
Nodejs探秘:深入理解单线程实现高并发原理
Node.js 子线程 crash 问题的排查
前言:昨天碰到了一个 worker_threads crash 的问题,最终经过阅读源码和调试找到了具体原因。不得不说,阅读源码是解决问题的非常有效的方法。
theanarkh
2022/07/01
8220
相关推荐
node.js 中的进程和线程工作原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档