序言
本文主要是为了记录自己的学习计划,将从模块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)
- 这里大致说下上述比较重要参数:
- 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()
})
})
- 这是上述代码的执行的结果,这个父子之间的通信是基于流的,说明底层是基于流来实现的
- 子类通知父类消息的时候,可以通过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()
})
- 上图是上述代码的运行结果:
- 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)
- 上述的截图是实例的执行结果:
- 上述的代码执行过程也是基于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())
})
- 上述截图是实例的运行结果:
- 首先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参数
结尾
好了大致的就是介绍这么多了。说了这么多了还是老样子,来个自我介绍