赞
踩
本篇文章对Node多进程源码进行剥丝抽茧,力图将多进程原理讲清,并且搞清楚exec,execFile,spawn,fork之间到底有什么关联,底层都是如何实现的。
默认视为你已了解了Node多进程,本文章使用当前最新的Node版本v16.1.0进行解析,由于最底层使用C++编写,超出了JS范畴,暂时不做解析。
先看一段使用代码:
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);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。