-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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 来执行命令
exec('"/path/to/test file/test.sh" arg1 arg2');
// 使用双引号,以便路径中的空格不被解析为多个参数的分隔符。
exec('echo "The \\$HOME variable is $HOME"');
// 第一个 $HOME 变量会被转义,第二个则不会。
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
封装
// 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
方法里
先看下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的内建模块代码阅读
在这里,node进行初始化的时候,就会判断NODE_CHANNEL_FD
变量,然后建立ipc通道。这个代码在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
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做了这么几件事情
- 生成了一个包裹函数,用于生成子进程
- 生成子进程,如果错误,返回其结果
- 如果有错,直接退出子进程,并关闭所有的流通道以及回收实例内存
- 如果没出错且ipc不为空的情况下,建立父子进程的通讯通道
注意,子进程的ipcFd
即NODE_CHANNEL_FD
标识,由child_process
模块处理。cluster中,调用fork方法,不需要对这块做处理,所以ipc通道可以进行父子进程的建立。
总结
因为c++能力有限,无法继续深入探究c++层面的代码。后续把c++以及操作系统的知识补充回来,再继续深入探究
Activity