通過源碼分析nodejs的進程架構

我們知道nodejs是單進程(單線程)的,但是nodejs也爲用戶實現了多進程的能力,下面我們看一下nodejs裏多進程的架構是怎麼樣的。
    nodejs提供同步和異步創建進程的方式。我們首先看一下異步的方式,nodejs創建進程的方式由很多種。但是歸根到底是通過spawn函數。所以我們從這個函數開始,看一下整個流程。

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函數(只列出核心代碼)。

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++模塊。

NODE_BUILTIN_MODULE_CONTEXT_AWARE(process_wrap, node::ProcessWrap::Initialize)

即在js層調用process.binding(‘process_wrap’)的時候,拿到的是node::ProcessWrap::Initialize導出的對象。

  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大概如下。

function New() {}
New.prototype = {
	spawn: Spawn,
	kill: Kill,
	close: Close
	...
}
module.exports = {Process: New}

所以new Process的時候,執行的是new New。

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函數沒有返回一個值。繼續往下看。

HandleWrap::HandleWrap(...) {
  Wrap(object, this);
}

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

v8的套路有點複雜,大致就是在FunctionCallbackInfo對象裏保存了ProcessWrap類的對象。後續調用的時候會取出來。然後給js返回一個對象。接着

this._handle.spawn(options);

這時候會執行

  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隊列,最後執行已退出進程的回調。

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。過程就不詳細說明了,直接看核心代碼。

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原來的事件循環。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章