深入理解Node.js多进程

前提

本篇文章对Node多进程源码进行剥丝抽茧,力图将多进程原理讲清,并且搞清楚exec,execFile,spawn,fork之间到底有什么关联,底层都是如何实现的。
默认视为你已了解了Node多进程,本文章使用当前最新的Node版本v16.1.0进行解析,由于最底层使用C++编写,超出了JS范畴,暂时不做解析。

spawn

先看一段使用代码:

const child = require('child_process')

const spawn = child.spawn('ls', ['-al'])
spawn.stdout.on('data', chunk => console.log(chunk))

这段代码会输出当前执行环境的文件目录,但是spawn替你做了些什么?

function spawn(file, args, options) {
   
  // 将参数进行格式化
  options = normalizeSpawnArguments(file, args, options);
  // 如果显式声明了timeout,需要是整数且大于0
  validateTimeout(options.timeout);
  // 如果显式声明了signal,要求必须是一个对象且该对象必须包含aborted属性
  validateAbortSignal(options.signal, 'options.signal');
  // 处理进程杀死的信号值,必须要求是number或string,且必须与内置的信号值对应
  const killSignal = sanitizeKillSignal(options.killSignal);
  // 创建子进程对象
  const child = new ChildProcess();
  // debug打印
  debug('spawn', options);
  // 执行子进程
  child.spawn(options);
  // 如果设置了timeout,此时开始实现:如果超过规定时间直接杀死子进程
  if (options.timeout > 0) {
   
    let timeoutId = setTimeout(() => {
   
      if (timeoutId) {
   
        try {
   
          child.kill(killSignal);
        } catch (err) {
   
          child.emit('error', err);
        }
        timeoutId = null;
      }
    }, options.timeout);
    // 子进程退出时,检查一遍内存
    child.once('exit', () => {
   
      if (timeoutId) {
   
        clearTimeout(timeoutId);
        timeoutId = null;
      }
    });
  }
  // 如果设置了signal,此时开始实现:如果signal.aborted为true,则函数结束后开始检查,否则等到abort再检查
  if (options.signal) {
   
    const signal = options.signal;
    if (signal.aborted) {
   
      process.nextTick(onAbortListener);
    } else {
   
      signal.addEventListener('abort', onAbortListener, {
    once: true });
      child.once('exit', () =>
        signal.removeEventListener('abort', onAbortListener)
      );
    }

    function onAbortListener() {
   
      // 如果在执行时子进程已经杀死,会报错
      abortChildProcess(child, killSignal);
    }
  }

  return child;
}

可以看到spawn的核心实现在于ChildProcess类,那么接下来就着重于分析ChildProcess类实现。首先分析处理函数。

格式化参数

function normalizeSpawnArguments(file, args, options) {
   
  // 校验第一个参数是不是string
  validateString(file, 'file');
  // 校验第一个参数是不是一个空串
  if (file.length === 0)
    throw new ERR_INVALID_ARG_VALUE('file', file, 'cannot be empty');
  // 第二个参数是不是数组
  if (ArrayIsArray(args)) {
   
    // 做一层浅拷贝
    args = ArrayPrototypeSlice(args);
  } else if (args == null) {
   
  	// 如果没有写第二个参数,给一个默认值
    args = [];
  } else if (typeof args !== 'object') {
   
  	// 如果第二个参数不是对象,报错
    throw new ERR_INVALID_ARG_TYPE('args', 'object', args);
  } else {
   
  	// 到这一步说明第二个参数是一个对象,将第二第三参数互调,视为options
    options = args;
    args = [];
  }
  // 给options赋默认值
  if (options === undefined) options = {
   };
  // 如果存在options,校验是否为对象
  else validateObject(options, 'options');

  // 如果显示声明了cwd配置属性,会对cwd配置属性进行string类型校验
  if (options.cwd != null) {
   
    validateString(options.cwd, 'options.cwd');
  }

  // 如果显示声明了detached配置属性,会对detached配置属性进行boolean类型校验
  if (options.detached != null && typeof options.detached !== 'boolean') {
   
    throw new ERR_INVALID_ARG_TYPE(
      'options.detached',
      'boolean',
      options.detached
    );
  }

  //  如果显示声明了uid配置属性,会对uid配置属性进行number类型校验
  if (options.uid != null && !isInt32(options.uid)) {
   
    throw new ERR_INVALID_ARG_TYPE('options.uid', 'int32', options.uid);
  }

  // 如果显示声明了gid配置属性,会对gid配置属性进行number类型校验
  if (options.gid != null && !isInt32(options.gid)) {
   
    throw new ERR_INVALID_ARG_TYPE('options.gid', 'int32', options.gid);
  }

  // 如果显示声明了shell配置属性,会对shell配置属性进行boolean, string类型校验
  if (
    options.shell != null &&
    typeof options.shell !== 'boolean' &&
    typeof options.shell !== 'string'
  ) {
   
    throw new ERR_INVALID_ARG_TYPE(
      'options.shell',
      ['boolean', 'string'],
      options.shell
    );
  }

  // 如果显示声明了argv0配置属性,会对argv0配置属性进行string类型校验
  if (options.argv0 != null) {
   
    validateString(options.argv0, 'options.argv0');
  }

  // 如果显示声明了windowHide配置属性,会对windowHide配置属性进行boolean类型校验
  if (options.windowsHide != null && typeof options.windowsHide !== 'boolean') {
   
    throw new ERR_INVALID_ARG_TYPE(
      'options.windowsHide',
      'boolean',
      options.windowsHide
    );
  }

  // 如果显示声明了windowsVerbatimArguments配置属性,会对windowsVerbatimArguments配置属性进行boolean类型校验
  let {
    windowsVerbatimArguments } = options;
  if (
    windowsVerbatimArguments != null &&
    typeof windowsVerbatimArguments !== 'boolean'
  ) {
   
    throw new ERR_INVALID_ARG_TYPE(
      'options.windowsVerbatimArguments',
      'boolean',
      windowsVerbatimArguments
    );
  }

  if (options.shell) {
   
  	// 如果存在shell配置属性,说明要进行自定义脚本配置,这里做一个join操作是为了更改了file为shell脚本路径之后,command不会受其改变而变化
    const command = ArrayPrototypeJoin([file, ...args], ' ');
    // 如果是windows,需要做特殊处理
    if (process.platform === 'win32') {
   
      // 如果你设置的就是string类型,说明你已经决定好该使用什么来执行node,直接设置即可
      if (typeof options.shell === 'string') file = options.shell;
      // 否则node帮你选择默认的脚本执行命令
      else file = process.env.comspec || 'cmd.exe';
      // 再次匹配是否是cmd.exe
      if (RegExpPrototypeTest(/^(?:.*\\)?cmd(?:\.exe)?$/i, file)) {
   
        // 为cmd.exe添加三个参数,这三个参数只能是cmd.exe使用
        args = ['/d', '/s', '/c', `"${
     command}"`];
        // 如果是cmd.exe,默认不为windows参数加上引号或转义
        windowsVerbatimArguments = true;
      } else {
   
        // 如果自己定义了shell且不是cmd.exe,正常为其添加-c
        args = ['-c', command];
      }
    } else {
   
      // 同理,判断其他平台并修改file,最后添加-c,
      if (typeof options.shell === 'string') file = options.shell;
      else if (process.platform === 'android') file = '/system/bin/sh';
      else file = '/bin/sh';
      args = ['-c', command];
    }
  }
  
  // 此时,会将argv0设置到最前面
  if (typeof options.argv0 === 'string') {
   
    ArrayPrototypeUnshift(args, options.argv0);
  } else {
   
    ArrayPrototypeUnshift(args, file);
  }
  // 如果显式声明了env,则直接取;否则从默认环境拿
  const env = options.env || process.env;
  const envPairs = [];

  // 确保env.NODE_V8_COVERAGE存在,NODE_V8_COVERAGE可通过对应的工具收集和分析覆盖率
  if (
    process.env.NODE_V8_COVERAGE &&
    !ObjectPrototypeHasOwnProperty(options.env || {
   }, 'NODE_V8_COVERAGE')
  ) {
   
    env.NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE;
  }

  let envKeys = [];
  // 将env的key都推入一个数组,这里是有意的包含原型链属性
  for (const key in env) {
   
    ArrayPrototypePush(envKeys, key);
  }

  if (process.platform === 'win32') {
   
    // Windows环境的env不区分大小写,为了避免重复,只取第一个env,之后的都过滤掉
    const sawKey = new SafeSet();
    envKeys = ArrayPrototypeFilter(ArrayPrototypeSort(envKeys), (key) => {
   
      const uppercaseKey = StringPrototypeToUpperCase(key);
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值