Node篇~~ 一文让你看懂子进程

1,068 阅读2分钟

序言

本文主要是为了记录自己的学习计划,将从模块child_process 中的spawn/ fork/ exec/ execFile 四个方面来来诠释node中创建子进程来执行

注意

如果大家对node一知半解,同样对模块child_process不甚了解,最好是先看下官方文档来看这篇文章更加好理解

下面实例代码的地址

实例代码地址参照

spawn

默认输出共享

// parent.js
const { spawn } = require('child_process')

const cp = spawn('node', ['son.js'], {

  cwd: __dirname,

  stdio: [process.stdin, process.stdout, process.stderr]

})

// 监听子进程的关闭状态
cp.on('close', () => {
  console.log('子进程关闭了')
})

// 监听子进程关闭状态
cp.on('exit', () => {
  console.log('子进程退出了')
})

// son.js
let sum = 0,
  i = 0
for (; i < 100 * 2000 * 4000; i++) sum += i
console.log(sum)
  • 图片.png
  • 这里大致说下上述比较重要参数:
    • cwd 表示当前命令运行工作目录
    • stdio 子进程的标准输入输出配置,其实spwan配置的默认参数{stdio}的值就是[0, 1, 2], 表示子类的输入输出跟父类共享。怎么理解数据共享呢??? 其实可以理解为console.log这个函数使用的是父类的,所以打印的时候输出到父类的控制端了。如果同学们不信可以把stdio参数修改为[0, null, 2]。这样就不会打印了。因为null表示不会共享
    • 那如果其实不想进行输出呢。我想让子程序告诉我 它已经完成了,那就请继续往下看!!!

基于pie的实现

// parent.js
const { spawn } = require('child_process')
const cp = spawn('node', ['son.js'], {
  cwd: __dirname,
  // 进程之间的通信 是基于流来实现的
  stdio: ['pipe', 'pipe', 'pipe']
})

// 订阅子类给父类 基于流传递的数据
cp.stdout.on('data', (e) => {
  console.log(e.toString())
})

// 订阅错误信息
cp.on('error', (e) => {
  console.log('error: ', e.toString())
})

// 基于流 给子类写数据
cp.stdin.write('父给子类数据')

// 订阅进程关闭
cp.on('close', () => {
  console.log('子进程关闭了')
})

// 订阅进程退出
cp.on('exit', () => {
  console.log('子进程退出了')
})

// son.js
let sum = 0,
  i = 0
  
for (; i < 100 * 2020 * 2300; i++) sum += i

process.stdout.write(String(sum))

// 订阅父类给子类的数据
process.stdin.on('data', (e) => {
  console.log(e.toString())
  process.nextTick(() => {
    process.kill()
  })
})
  • 图片.png
  • 这是上述代码的执行的结果,这个父子之间的通信是基于流的,说明底层是基于流来实现的
    • 子类通知父类消息的时候,可以通过write写入,而父类可以监听流的变化来获取数据

fork

可以理解为fork其实是spwan的ipc通信的进阶版,而且fork就是基于ipc通信来实现的,后续在分析源码的时候会进行分析

实例解析

// parent.js
const { fork } = require('child_process')

const cp = fork('son.js', {
  cwd: __dirname
})

cp.on('message', (e) => {
  console.log(e)
})

cp.send('子类你好啊', function () {})

cp.on('close', function () {
  console.log('子进程关闭了')
})

cp.on('exit', function () {
  console.log('子进程退出了')
})

// son.js
let sum = 0,
  i = 0
for (; i < 100 * 3000 * 300; i++) sum += i

process.send('计算结果是:' + sum)
process.on('message', function (e) {
  console.log(e)

  process.exit()
})
  • 图片.png
  • 上图是上述代码的运行结果:
    • fork是spwan的进阶版
    • fork天生就是基于ipc通道进行通信的,输入输出标准中是[0, 1, 2, 'ipc']
    • 是通过send函数,以及监听message来进行消息通信
    • 一旦开始通信了无法自动关闭通道,因为已经建立了一个通道
    • fork默认就是使用node来执行的

源码解析

下列源码中删除了不重要的部分,node源码中child_process.js 中

function fork(modulePath /* , args, options */) {
  // 判断是否是有效字符串
  validateString(modulePath, 'modulePath');

  // Get options and args arguments.
  let execArgv;
  let options = {};
  let args = [];
  let pos = 1;

  if (pos < arguments.length && arguments[pos] != null) {
    if (typeof arguments[pos] !== 'object') {
      throw new ERR_INVALID_ARG_VALUE(`arguments[${pos}]`, arguments[pos]);
    }

    // 进行参数合并 合并参数cwd 以及stdio等参数
    options = { ...arguments[pos++] };
  }

  // Prepare arguments for fork:
  execArgv = options.execArgv || process.execArgv;

  args = execArgv.concat([modulePath], args);

  if (typeof options.stdio === 'string') {
    options.stdio = stdioStringToArray(options.stdio, 'ipc');

    // 如果进行fork的时候 不设置stdio 会执行这个处理
  } else if (!ArrayIsArray(options.stdio)) {
    // Use a separate fd=3 for the IPC channel. Inherit stdin, stdout,
    // and stderr from the parent if silent isn't set.
    // 最后这个转换结果是[0, 1, 2, 'ipc']
    options.stdio = stdioStringToArray(
      options.silent ? 'pipe' : 'inherit',
      'ipc');
  } else if (!options.stdio.includes('ipc')) {
    throw new ERR_CHILD_PROCESS_IPC_REQUIRED('options.stdio');
  }

  // 设置node的地址
  options.execPath = options.execPath || process.execPath;
  // 将执行shell 设置为false
  options.shell = false;

  // 其实fork也是基于spawn进行进程之间的通信
  return spawn(options.execPath, args, options);
}
  • 上述是fork源码的解析,其实就是基于spawn来进行通信的,只不过是默认的通道方式就是ipc,而且必须是ipc

execFile

实例解析

// parent.js
const { execFile } = require('child_process')

// 直接执行文件
const cp = execFile('node', ['son.js'], {
  cwd: __dirname
}, function(err, stdout, stderr) {
  // 通过回调来触发结果
  console.log(stdout)
})

cp.on('close', function() {
  console.log('子进程关闭了')
})

cp.on('exit', function() {
  console.log('子进程退出了')
})

// son.js
let sum = 0, i = 0
for (; i < 100 * 300 * 400; i ++) sum += i

console.log(sum)
  • 图片.png
  • 上述的截图是实例的执行结果:
    • 上述的代码执行过程也是基于spawn的
    • execFile执行过程中,默认就是shell:false
    • 参数stdio 都是默认属性[0, 1, 2]

源码解析

function execFile(file /* , args, options, callback */) {
  let args = [];
  let callback;
  let options;

  // Parse the optional positional parameters.
  let pos = 1;
  if (pos < arguments.length && ArrayIsArray(arguments[pos])) {
    // 待执行的文件 或是 待执行的命令
    args = arguments[pos++];
  } else if (pos < arguments.length && arguments[pos] == null) {
    pos++;
  }

  if (pos < arguments.length && typeof arguments[pos] === 'object') {
    options = arguments[pos++];
  } else if (pos < arguments.length && arguments[pos] == null) {
    pos++;
  }

  if (pos < arguments.length && typeof arguments[pos] === 'function') {
    // 用来获取回调函数
    callback = arguments[pos++];
  }

  if (!callback && pos < arguments.length && arguments[pos] != null) {
    throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]);
  }

  options = {
    encoding: 'utf8',
    timeout: 0,
    maxBuffer: MAX_BUFFER,
    killSignal: 'SIGTERM',
    cwd: null,
    env: null,
    shell: false,
    ...options
  };

  // 说明execFile 底层还是基于spawn来实现的
  const child = spawn(file, args, {
    cwd: options.cwd,
    env: options.env,
    gid: options.gid,
    shell: options.shell,
    signal: options.signal,
    uid: options.uid,
    windowsHide: !!options.windowsHide,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments
  });

exec

const { exec } = require('child_process')

exec('path', {cwd: __dirname}, function(err, stdout, stderr) {
  console.log(stdout)
  console.log(stderr.toString())
})
  • 图片.png
  • 上述截图是实例的运行结果:
    • 首先exec命令 可以通过字符串执行写命令 来执行
    • exec命令中属性shell默认就是true
    • exec是基于execFile来实现的

源码解析

function exec(command, options, callback) {
  // 在函数normalizeExecArgs中来设置 默认的shell是true
  const opts = normalizeExecArgs(command, options, callback);
  return module.exports.execFile(opts.file,
                                 opts.options,
                                 opts.callback);
}

看,exec函数的实现原理是不是很简单啊

结论:

  • 通过上述的实例中会发现,除了spawn以外,其余的都是多多少少基于spawn来实现的
  • spawn中父子进程通信的方式有多种,除了默认(process.stdin/ stdout)的以外,还有基于流的pie,以及基于ipc的消息通道
  • fork的子进程创建本身就是基于node的
  • 而exec函数 以及execFile差别在于shell参数

结尾

好了大致的就是介绍这么多了。说了这么多了还是老样子,来个自我介绍