1. Node多进程child_process库exec方法源码执行流程分析
疑问和收获:
- exec和execFile到底有什么区别?
- 为什么exec/execFile/fork都是通过spawn实现的,spawn的作用到底是什么?
- 为什么spawn调用后没有回调,而exec和execFile能够回调?
- 为什么spawn调用后需要手动调用child.stdout.on(‘data’,callback),这里的child.stdout/child.stderr到底是什么?
- 为什么有data/error/exit/close这么多种回调,他们的执行顺序到底是怎样的?
exec 源码深入分析
在未学习exec源码之前,我们先对上面的拓扑图进行一个简单的学习,看到exec内部的执行流程 不难看到exec执行的是execlFile这个方法,且不同的地方就是传入的参数不同,而execFile执行的是spawn这个方法,且spawn这个方法调用的是node内部库的一个child_process方法。
我们在webstorm中打开一个项目
// /bin/process/index.js
const cp = require('child_process')
const path = require('path')
cp.exec('ls -al|grep node_modules ',function(err,stdout,stderr){
console.log(err)
console.log(stdout)
console.log(stderr)
})
1、通过断点调试可以知道,exec
方法实际上只是调用了normalizeExecArgs
方法,对参数进行了处理,然后调用了execFile
方法
function exec(command, options, callback) {
const opts = normalizeExecArgs(command, options, callback);
return module.exports.execFile(opts.file,
opts.options,
opts.callback);
}
2、execFile 内部也是调用了spawn
方法,把参数格式化成了这种形式:
指定了执行命令的终端/bin/sh
args: ['/bin/sh', '-c', 'ls -al|grep node_modules ']
3、spawn
内部也是调用new ChildProcess()
方法,创建了子进程来执行实例child
上的spawn
方法。
function spawn(file, args, options) {
options = normalizeSpawnArguments(file, args, options);
validateTimeout(options.timeout);
validateAbortSignal(options.signal, 'options.signal');
const killSignal = sanitizeKillSignal(options.killSignal);
const child = new ChildProcess();
debug('spawn', options);
// 调用的是子进程child的spawn方法
child.spawn(options);
ChildProcess
调用的是C++的内置库,帮我们创建一个子进程。
const { Process } = internalBinding('process_wrap');
在监视器中打印process.pid
可以看出当前进程是80796,然后 ps -ef | grep 80796
可以查看当前的子进程id 80797。
wangjiayan@wangjiayandeMacBook-Pro-2 test-cli % ps -ef | grep 80796
501 80796 79802 0 5:28下午 ?? 0:00.21 /usr/local/bin/node ./bin/process.js
501 80797 80796 0 5:28下午 ?? 0:00.16 /usr/local/bin/node /private/var/folders/hl/7kpq3z6j59l5pkywt742tqnw0000gn/T/AppTranslocation/DFC0361D-C073-4F57-907F-256B6C4DAD07/d/Visual Studio Code.app/Contents/Resources/app/extensions/ms-vscode.js-debug/src/watchdog.bundle.js
501 80907 80796 0 5:37下午 ?? 0:00.00 (bash)
4、spawn
方法,创建三个管道通信stdio
,实现了消息订阅接口ee,并且给每个输入输出流实现了socket接口,
深度分析child_process库spawn底层实现
接着上一节代码块中走到了child.spawn
:
- 第一步是通过
getValidStdio
去生成pipe,创建一个管道实例:第一个是输入,第二个是输出,第三个是error(只是生成了管道,但是还没创建socket的通信) - 第二步对
spawn
的一些参数进行处理:下面代码未贴 - 第三步通过
this._handle.spawn
子进程被创建出来 第四步通过
createSocket
方法,将之前的pipe和子进程与socket绑定。ChildProcess.prototype.spawn = function(options) {
...
// 创建三个管道通信
stdio = getValidStdio(stdio, false);
...
for (i = 0; i < stdio.length; i++) {
const stream = stdio[i];
....
if (stream.handle) {
....
stream.socket = createSocket(this.pid !== 0 ?
stream.handle : null, i > 0);
if (i > 0 && this.pid !== 0) {
this._closesNeeded++;
stream.socket.on('close', () => {
maybeClose(this);
});
}
}
}
}
5、spawn方法回调原理,主要分2条线。方便在回调的时候将stdout
一次性返回。
子进程的执行线,由事件订阅实现,子进程退出后执行
cp.on('exit',function(code){
console.log('exit',exit)
})
流读取:由socket实现,这个是流关闭后去触发
cp.stdout.on('data',function(chunk){
console.log('stdout',chunk.toString())
})
spawn 底层是调用了C++创建子进程并执行子进程的方法,
Node多进程源码总结
exec/execFile/spawn/fork的区别
- exec: 原理是调用/bin/sh -c 执行我们传入的shell脚本,底层调用略execFile
- execFile:原理是直接执行我们传入的file和args,底层调用spawn创建和执行子进程,并建立略回调,一次性将所有的stdout和stderr结果返回
- spawn:原理是调用略internal/child_process,实例化略ChildProcess子进程对象,再调用child.spawn创建 子进程并执行命令,底层是调用了child.)handle.spawn执行process_wrap中的spwan方法,执行过程是异步的,执行完毕后再通过PIPE进行单向数据通信,通信结束后子进程发起onexit回调,同时Socket会执行close回调。
fork:原理是通过spawn创建子进程和执行命令,采用node执行命令,通过setupchannel创建IPC用于子进程和父进程之间的双向通信。
data/error/exit/close回调的区别
data:用于主进程读取数据过程中通过onStreamRead发起的回调
- error: 命令执行失败后发起的回调
- exit: 子进程关闭完成后发起的回调
- close:子进程所有Socket通信端口全部关闭后发起的回调
stdout close/stderr close:特定的PIPE读取完成后调用onReadableStreamEnd关闭Socket时发起的回调。
要点1:对参数的处理
child_process.exec(command[, options][, callback])
这里如果传入两个参数,对参数做了处理成了三个参数。 ```javascript function normalizeExecArgs(command, options, callback) { if (typeof options === ‘function’) { callback = options; options = undefined; }// Make a shallow copy so we don’t clobber the user’s options object. // 这里有疑惑,浅拷贝改变了,源对象,也会跟着改变啊 options = { …options }; options.shell = typeof options.shell === ‘string’ ? options.shell : true;
return { file: command, options: options, callback: callback }; }
<a name="Q9L2m"></a>
## 要点2:终端执行命令的原理
执行命令`ls`时,相当于`/bin/sh -c "ls"`<br />`-c` :<command><br />`/bin/sh`后面既可以加命令,也可以加可执行文件。script-file
<a name="KCwke"></a>
## 要点3查看当前进程及子进程
判断当前程序执行进程的方法,代码调用`process.pid`拿到当前进程id。然后再
```javascript
ps -ef | grep [pid]