前提
本篇文章对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);