Open EasonYou opened 4 years ago
child_process 模块提供了衍生子进程的能力,主要由child_process.spawn()函数提供
child_process
child_process.spawn()
child_process.spawn()方法异步衍生子进程,不阻塞Node.js事件循环。
child_process.spawnSync()提供一样的功能,但是会阻塞时间循环知道进程退出或终止
child_process.spawnSync()
其他的方法(同步与异步),都是基于以上两个方法实现的,只是方便用户使用。
在Unix 类型的操作系统,child_process.execFile()会比child_process.exec()性能更好,因为默认情况下不会衍生shell
child_process.execFile()
child_process.exec()
但在windows下,.bat和.cmd文件没有终端情况下不能自行执行,因此无法使用child_process.execFile()启动。
执行.bat和.cmd可以使用设置了shell选项的child_process.spawn()或child_process.exec()
衍生一个shell然后在该shell中执行command,并缓冲任何产生的输出
可提供callback,调用时传入参数(error, stdout, stderr)。error是Error的实例,error.code是子进程的退出码,error.signal被设为终止进程的信号,除0以外的退出码都被视为错误。
(error, stdout, stderr)
error
Error
error.code
error.signal
child_process.exec()不会替换现有的进程,且使用 shell 来执行命令
exec('"/path/to/test file/test.sh" arg1 arg2'); // 使用双引号,以便路径中的空格不被解析为多个参数的分隔符。 exec('echo "The \\$HOME variable is $HOME"'); // 第一个 $HOME 变量会被转义,第二个则不会。
child_process.execFile() 函数类似于 child_process.exec(),但默认情况下不会衍生 shell。 相反,指定的可执行文件 file 会作为新进程直接地衍生,使其比 child_process.exec() 稍微更高效。
execFile是exec的一层封装,底层调用的就是exec。
execFile
exec
child_process.fork()专门用于衍生新的Node.js进程,一样返回ChildProcess对象,其将会内置一个额外的通信通道,允许信息在父子进程间来回传递。(subprocess.send())
child_process.fork()
ChildProcess
subprocess.send()
除了衍生进程外,内容和V8实例等,也需要额外分配资源
不设置的情况下,会使用父进程的process.execPath来衍生新的Node.js实例,可通过设置,允许使用其他的执行路径。
process.execPath
使用自定义的 execPath 启动的 Node.js 进程将会使用文件描述符(在子进程上使用环境变量 NODE_CHANNEL_FD 标识)与父进程通信
child_process.fork() 不会克隆当前的进程,且不支持shell选项
child_process.spawn() 方法使用给定的 command 衍生一个新进程,并带上 args 中的命令行参数。
进程模块,总的来说在使用上比较简单,没有很复杂的地方。还有几个同步创建子进程的方法,与上面的异步方法雷同,不再赘述。
但是需要深入了解各个方法的核心调用,理解操作系统层面的一些问题。后续需要深入node的这块的源码。
child_process在JavaScript的核心模块上,还是比较简单的,派生进程的实现都交给了内建模块的c++实现了
从源码上,可以很好地阐述,node是如何基于spwan去派生出其他方法的
从文档中可得知,fork/exec/execFile三个方法都是基于spwan封装的,其中exec基于execFile封装
fork
// exec中,将file options做了一层封装,直接调用的execFile function exec(command, options, callback) { const opts = normalizeExecArgs(command, options, callback); return module.exports.execFile(opts.file, opts.options, opts.callback); } // execFile主要还是通过spawn来进行子进程派生 // 后面更多的是对退出和错误事件的监听以及stdout和stderr的输出处理 function execFile(file /* , args, options, callback */) { //.. // 派生子进程 const child = spawn(file, args, { cwd: options.cwd, env: options.env, gid: options.gid, uid: options.uid, shell: options.shell, windowsHide: !!options.windowsHide, windowsVerbatimArguments: !!options.windowsVerbatimArguments }); //.. // 退出事件 function exithandler(code, signal) { } // 错误事件 function errorhandler(e) { } // 信息传递 function kill() { } // .. // 缓存stdout和stderr if (child.stdout) { if (encoding) child.stdout.setEncoding(encoding); // 监听data事件 child.stdout.on('data', function onChildStdout(chunk) { const encoding = child.stdout.readableEncoding; // 如果是buffer类型,则加上收到的字节数,否则,加上收到的字符串 const length = encoding ? Buffer.byteLength(chunk, encoding) : chunk.length; stdoutLen += length; if (stdoutLen > options.maxBuffer) { // 判断是否超出内部的buffer const truncatedLen = options.maxBuffer - (stdoutLen - length); _stdout.push(chunk.slice(0, truncatedLen)); // 缓存字符串 kill(); } else { _stdout.push(chunk); // 缓存buffer } }); } if (child.stderr) { // .. 忽略,处理方式跟data差不多 } // 监听事件 child.addListener('close', exithandler); child.addListener('error', errorhandler); return child; } // fork,主要还是调用的spawn方法 // 主要是针对node的调用路径,做了参数上的序列化 function fork(modulePath /* , args, options */) { // ... // 如果没有execPath,则派生当前进程的路径 options.execPath = options.execPath || process.execPath; // 不以shell的形式启动 options.shell = false; return spawn(options.execPath, args, options); }
从上面的代码可以看出spawn方法才是最关键的方法,基本都是基于spawn去做参数的封装
spawn
首先来看下spawn方法里面做了什么
// lib/child_process.js // 在内建模块上,去引入child_process const child_process = require('internal/child_process'); // 这里,我们只关注ChildProcess这个构造方法 const { ChildProcess } = child_process; // spawn方法,由于派生子进程 function spawn(file, args, options) { // 序列化参数 const opts = normalizeSpawnArguments(file, args, options); // 生成一个childprocess实例 const child = new ChildProcess(); // 通过spwan方法进行子进程的派生 child.spawn({ /** ... */ }); return child; }
从上面可以看出,其实spawn方法只是new了一个ChildProcess的实例,然后调用了实例的spawn方法
接下来看下ChildProcess内部是怎么运作的
// lib/internal/child_process.js function ChildProcess() { // 继承了EventEmitter EventEmitter.call(this); //.. // 生成一个进程的派生句柄,并没有在这里直接派生进程 this._handle = new Process(); this._handle[owner_symbol] = this; // 绑定onexit时间 this._handle.onexit = (exitCode, signalCode) => { // ..这里做了一些进程退出的事件操作,忽略 }; } // 这里是真正派生进程的方法 ChildProcess.prototype.spawn = function(options) { let i = 0; // .. // 默认为pipe的标准释出 let stdio = options.stdio || 'pipe'; stdio = getValidStdio(stdio, false); // .. if (ipc !== undefined) { // 让子进程知道IPC通道 options.envPairs.push(`NODE_CHANNEL_FD=${ipcFd}`); options.envPairs.push(`NODE_CHANNEL_SERIALIZATION_MODE=${serialization}`); } // 获取file等参数 this.spawnfile = options.file; if (ArrayIsArray(options.args)) this.spawnargs = options.args; else if (options.args === undefined) this.spawnargs = []; const err = this._handle.spawn(options); // 进程生成错误,在nextTick的时候抛出 if (/** .. */) { process.nextTick(onErrorNT, this, err); } else if (err) { // 关闭所有的pipe管道 for (i = 0; i < stdio.length; i++) { const stream = stdio[i]; if (stream.type === 'pipe') { stream.handle.close(); } } this._handle.close(); // 关闭进程 this._handle = null; // 回收内存 throw errnoException(err, 'spawn'); // 抛出错误 } // 忽略... // 建立父子进程的通讯通道 if (ipc !== undefined) setupChannel(this, ipc, serialization); return err; };
在主进程执行spawn前,会创建一个管道作为ipc通道,这个创建就在getValidStdio方法里
getValidStdio
先看下stdio的序列化
stdio
原始的stdio可能为3个值
ignore
['ignore', 'ignore', 'ignore']
pipe
['pipe', 'pipe', 'pipe']
inherit
['inherit', 'inherit', 'inherit']
[0, 1, 2]
// 序列化stdio function stdioStringToArray(stdio, channel) { const options = []; switch (stdio) { case 'ignore': case 'pipe': options.push(stdio, stdio, stdio); break; case 'inherit': options.push(0, 1, 2); break; default: throw new ERR_INVALID_OPT_VALUE('stdio', stdio); } // ... return options; }
先序列化后,再对对应的stdio创建ipc通道
function getValidStdio(stdio, sync) { // ...stdio可能为 ignore/ if (typeof stdio === 'string') { stdio = stdioStringToArray(stdio); } // 创建ipc通道 stdio = stdio.reduce((acc, stdio, i) => { // ... if (stdio === 'ignore') { acc.push({ type: 'ignore' }); } else if (stdio === 'pipe' || (typeof stdio === 'number' && stdio < 0)) { // ... } else if (stdio === 'ipc') { // ... // 创建ipc通道 ipc = new Pipe(PipeConstants.IPC); ipcFd = i; acc.push({ type: 'pipe', handle: ipc, ipc: true }); } else if (stdio === 'inherit') { // ... } else if (typeof stdio === 'number' || typeof stdio.fd === 'number') { // ... } else if (getHandleWrapType(stdio) || getHandleWrapType(stdio.handle) || getHandleWrapType(stdio._handle)) { // ... } else if (isArrayBufferView(stdio) || typeof stdio === 'string') { /** ... */} else { /** ... */} return acc; }, []); return { stdio, ipc, ipcFd }; }
Pipe内部构造了一个双工流,本质上这个管道跟stream是一样的模式。因此可以父子进程间互相通信 // todo.. pipe的内建模块代码阅读
Pipe
在这里,node进行初始化的时候,就会判断NODE_CHANNEL_FD变量,然后建立ipc通道。这个代码在lib/internal/bootstrap/pre_execution.js
NODE_CHANNEL_FD
lib/internal/bootstrap/pre_execution.js
function setupChildProcessIpcChannel() { if (process.env.NODE_CHANNEL_FD) { // 获取fd const fd = parseInt(process.env.NODE_CHANNEL_FD, 10); // 建立IPC通道 require('child_process')._forkChild(fd, serializationMode); assert(process.send); } }
在进程初始化的时候,设置进程通道,在lib/child_process.js
lib/child_process.js
function _forkChild(fd, serializationMode) { // 创建管道实例 const p = new Pipe(PipeConstants.IPC); p.open(fd); p.unref(); // 在这里设置IPC通道 const control = setupChannel(process, p, serializationMode); process.on('newListener', function onNewListener(name) { if (name === 'message' || name === 'disconnect') control.ref(); }); process.on('removeListener', function onRemoveListener(name) { if (name === 'message' || name === 'disconnect') control.unref(); }); } // todo... /lib/internal/child_process.js setupChannel
在JavaScript层面,node做了这么几件事情
注意,子进程的ipcFd即NODE_CHANNEL_FD标识,由child_process模块处理。cluster中,调用fork方法,不需要对这块做处理,所以ipc通道可以进行父子进程的建立。
ipcFd
因为c++能力有限,无法继续深入探究c++层面的代码。后续把c++以及操作系统的知识补充回来,再继续深入探究
childe_process 子进程
child_process
模块提供了衍生子进程的能力,主要由child_process.spawn()
函数提供child_process.spawn()
方法异步衍生子进程,不阻塞Node.js事件循环。child_process.spawnSync()
提供一样的功能,但是会阻塞时间循环知道进程退出或终止其他的方法(同步与异步),都是基于以上两个方法实现的,只是方便用户使用。
创建异步进程
在 Windows 上衍生 .bat 和 .cmd 文件(了解)
在Unix 类型的操作系统,
child_process.execFile()
会比child_process.exec()
性能更好,因为默认情况下不会衍生shell但在windows下,.bat和.cmd文件没有终端情况下不能自行执行,因此无法使用
child_process.execFile()
启动。执行.bat和.cmd可以使用设置了shell选项的
child_process.spawn()
或child_process.exec()
child_process.exec(command[, options][, callback])
衍生一个shell然后在该shell中执行command,并缓冲任何产生的输出
可提供callback,调用时传入参数
(error, stdout, stderr)
。error
是Error
的实例,error.code
是子进程的退出码,error.signal
被设为终止进程的信号,除0以外的退出码都被视为错误。child_process.exec()
不会替换现有的进程,且使用 shell 来执行命令child_process.execFile(file[, args][, options][, callback])
child_process.execFile()
函数类似于child_process.exec()
,但默认情况下不会衍生 shell。 相反,指定的可执行文件 file 会作为新进程直接地衍生,使其比child_process.exec()
稍微更高效。execFile
是exec
的一层封装,底层调用的就是exec
。child_process.fork(modulePath[, args][, options])
child_process.fork()
专门用于衍生新的Node.js进程,一样返回ChildProcess
对象,其将会内置一个额外的通信通道,允许信息在父子进程间来回传递。(subprocess.send()
)除了衍生进程外,内容和V8实例等,也需要额外分配资源
不设置的情况下,会使用父进程的
process.execPath
来衍生新的Node.js实例,可通过设置,允许使用其他的执行路径。使用自定义的 execPath 启动的 Node.js 进程将会使用文件描述符(在子进程上使用环境变量 NODE_CHANNEL_FD 标识)与父进程通信
child_process.fork()
不会克隆当前的进程,且不支持shell选项child_process.spawn(command[, args][, options])
child_process.spawn()
方法使用给定的 command 衍生一个新进程,并带上 args 中的命令行参数。总结
进程模块,总的来说在使用上比较简单,没有很复杂的地方。还有几个同步创建子进程的方法,与上面的异步方法雷同,不再赘述。
但是需要深入了解各个方法的核心调用,理解操作系统层面的一些问题。后续需要深入node的这块的源码。
源码阅读
child_process
在JavaScript的核心模块上,还是比较简单的,派生进程的实现都交给了内建模块的c++实现了从源码上,可以很好地阐述,node是如何基于spwan去派生出其他方法的
fork/exec/execfile的实现
从文档中可得知,
fork
/exec
/execFile
三个方法都是基于spwan封装的,其中exec
基于execFile
封装从上面的代码可以看出
spawn
方法才是最关键的方法,基本都是基于spawn去做参数的封装子进程派生,spawn
首先来看下spawn方法里面做了什么
从上面可以看出,其实spawn方法只是new了一个
ChildProcess
的实例,然后调用了实例的spawn
方法接下来看下ChildProcess内部是怎么运作的
在主进程执行
spawn
前,会创建一个管道作为ipc通道,这个创建就在getValidStdio
方法里先看下
stdio
的序列化原始的
stdio
可能为3个值ignore
,不需要对数组做处理,直接返回一个空数组。在后续处理会处理成['ignore', 'ignore', 'ignore']
pipe
,相当于['pipe', 'pipe', 'pipe']
inherit
- 相当于['inherit', 'inherit', 'inherit']
或[0, 1, 2]
先序列化后,再对对应的
stdio
创建ipc通道Pipe
内部构造了一个双工流,本质上这个管道跟stream是一样的模式。因此可以父子进程间互相通信 // todo.. pipe的内建模块代码阅读在这里,node进行初始化的时候,就会判断
NODE_CHANNEL_FD
变量,然后建立ipc通道。这个代码在lib/internal/bootstrap/pre_execution.js
在进程初始化的时候,设置进程通道,在
lib/child_process.js
在JavaScript层面,node做了这么几件事情
注意,子进程的
ipcFd
即NODE_CHANNEL_FD
标识,由child_process
模块处理。cluster中,调用fork方法,不需要对这块做处理,所以ipc通道可以进行父子进程的建立。总结
因为c++能力有限,无法继续深入探究c++层面的代码。后续把c++以及操作系统的知识补充回来,再继续深入探究