child_process 模块提供了衍生子进程的能力。 此功能主要由 child_process.spawn() 函数提供,exec、execFile、fork 底层都是通过 spawn 实现的,它们都是异步衍生子进程的方式,同样 child_process 也提供了同步版本:
const { spawnSync, execSync, execFileSync } = require('child_process');
在讲述具体的创建子进程的方法前,先了解下父子进程分别指的是什么:
Node 执行代码(此处为node ./parent.js)衍生的进程叫父进程(主进程),通过 child_process 衍生(此处为执行 exec)的进程叫子进程。
//parent.js
const { exec } = require('child_process');
exec('ls');
本文的例子详见。
child_process.spawn(command[, args][, options])
spawn 方法使用给定的 command 和 args 中的命令行参数衍生新进程。
-
command(必选)
string 类型,表示要执行的命令,如果给的 command 不存在则报 ENOENT 错误。
-
args(可选)
string[]类型,表示参数列表
-
options(可选)
-
cwd()
string 或 URL 类型,表示子进程的工作目录,默认为
process.cwd()。如果给的 cwd 不存在则报 ENOENT 错误。 -
env
Object 类型,表示环境变量的键值对,默认为 process.env。
spawn('echo $ANSWER', { stdio: 'inherit', shell: true, env: { ANSWER: 20 }, }); //20 -
argv0
显式设置发送给子进程的 argv0 的值, 如果未指定,则设置为 command。
//设置argv0为abc则子进程的argv0为abc,否则为node spawn('node', ['./child.js'], { argv0: 'abc' }); -
stdio
Array或string类型,表示标准的输入输出,用来配置父子进程间的管道流。stdio 的可选值:
-
pipe
等同于
['pipe', 'pipe', 'pipe'](默认值),表示标准输入、标准输出和标准错误分别为 pipe、pipe、pipe。pipe 的父端作为子进程对象上的属性subprocess.stdio[fd]暴露给父进程,fd 为 0、1、2 分别对应 subprocess.stdin, subprocess.stdout 和 subprocess.stderr。 pipe 意味着,child.stdin、child.stdout 不是 undefined,可以通过监听 data 事件,来获取数据。const sub = spawn('ls', { stdio: 'pipe', }); console.log(sub.stdio[0] === sub.stdin); //true console.log(sub.stdio[1] === sub.stdout); //true console.log(sub.stdio[2] === sub.stderr); //true sub.stdout.on('data', function (data) { console.log(`data from child: ${data}`); });子进程的 stdio 是可写流,而父进程的 stdio 是可读流: 执行
node readable-pipe-writable.js,输入字符 abc(3 个字符),按ctrl+d(结束输入),父进程的输入会传递给子进程,wc 子进程计算输入字符的个数,输出 3://readable-pipe-writable.js const { spawn } = require('child_process'); const child = spawn('wc', ['-m']); //获取字符数 process.stdin.pipe(child.stdin); child.stdout.on('data', (data) => { console.log(data); //3 });在进程间同样可以 pipe:
//spawn-pipe-each-other.js const { spawn } = require('child_process'); const find = spawn('find', ['.', '-type', 'f']); const wc = spawn('wc', ['-l']); //计算行数 find.stdout.pipe(wc.stdin); wc.stdout.on('data', (data) => { console.log(`Number of files ${data}`); }); -
overlapped
等同于
['overlapped', 'overlapped', 'overlapped'],和 pipe 在非 Windows 系统上是完全一样的,详见。 -
ignore
等同于
['ignore', 'ignore', 'ignore'],Node.js 子进程忽略 fd,这意味着无法通过 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 获取数据。 -
inherit
等同于
['inherit', 'inherit', 'inherit']、[0, 1, 2]或[process.stdin, process.stdout, process.stderr]。表示父进程的 stdios 由子进程控制,如下: 当不设置 stdio,控制台执行node spawn-inherit.js不会输出任何内容,当 stdio 设置为 inherit 时会输出pwd结果,因为 inherit 使得子进程可以使用父进程的 stdios,也就能在父进程的控制台输出内容。//spawn-inherit.js const { spawn } = require('child_process'); spawn('pwd', { stdio: 'inherit', });
前述已经知道 fd 的 0、1 和 2 分别对应 stdin、stdout 和 stderr。 额外的 fd 可以被指定来衍生父进程和子进程之间的额外管道。 该值是以下之一:
-
ipc
衍生一个用于父子进程间传递消息或文件描述符的 IPC 通道符。设置该选项可启用
subprocess.send()方法。 如果子进程是一个 Node.js 进程,则一个已存在的 IPC 通道会在子进程中启用process.send()、process.disconnect()、process.on('disconnect')和process.on('message')。//spawn-ipc.js const { spawn } = require('child_process'); //如果不开ipc是没有subprocess.send方法的 const subprocess = spawn('node', ['./child.js'], { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], }); subprocess.on('message', function (message) { console.log(`Receive message from child: ${JSON.stringify(message)}`); }); subprocess.send({ from: 'parent' }); /*********************分割线*******************/ //如果不开ipc是没有process.send方法的 process.on('message', function (message) { console.log(`Receive message from parent: ${JSON.stringify(message)}`); }); process.send({ from: 'child' }); -
pipe
同上述 pipe。
-
ignore
同上述 ignore。
-
共享一个指向子进程的 tty(文本终端)、文件、socket 或管道的可读或可写流。
-
正整数
表示一个正在父进程中打开的文件描述符, 它和子进程共享。
-
null, undefined
默认值, 对于 stdio fd 0、1 和 2(换言之,stdin、stdout 和 stderr)而言是衍生了一个管道。 对于 fd 3 及以上而言,默认值为 'ignore'。
-
-
detached
boolean 类型, 表示子进程独立于父进程执行,默认情况下父进程会等待子进程退出后再退出,如果要阻止父进程等待子进程,可以用 subprocess.unref()方法,如果不调用
subprocess.unref(),父进程会等待子进程结束(10s 后)才会退出://spawn-detached.js const subprocess = spawn('node', ['long-task.js'], { detached: true, stdio: 'ignore', //不设置成ignore,调用unref父进程仍然不会退出 }); subprocess.on('close', (code) => { console.log(`subprocess process exited with code ${code}`); }); subprocess.unref(); /*-------------------分割线---------------------*/ //long-task.js setTimeout(() => { console.log('10 seconds'); }, 10000);执行
node spawn-detached.js后父进程退出,在控制台上查看 long-task.js:ps -ef | grep long-task.js => 501 42821 1 0 10:02AM ?? 0:00.02 node long-task.js可以看到父进程退出后子进程仍然在独立运行。
-
uid
number 类型,表示执行进程的 user id。
-
gid
number 类型,表示执行进程分组 id。
-
serialization
string 类型,表示进程间传递信息的序列化方法,可能的值是 json 和 advanced 默认值为 json。
-
shell
string 或 boolean 类型,默认值为 false,当为 true 时,在 Unix 上的 shell 为
/bin/sh,Windows 上为process.env.ComSpec。 默认情况下,spawn 并不会创建 Shell 来执行命令,因此通过 Shell 的方式执行命令会报错:const { spawn } = require('child_process'); spawn('ls -a'); //Error: spawn ls -a ENOENT const child = spawn('ls -a', { shell: true, }); //通过Shell来执行,不会报错 spawn('ls', ['-a']); //不会报错 -
windowsVerbatimArguments
布尔类型, 在 Windows 上不为参数加上引号或转义,默认值为 false,只在 Windows 上起作用。
-
windowsHide
boolean 类型,表示隐藏子进程的窗口(通常在 Windows 上会衍生),默认值为 false。
-
signal
类型为AbortSignal,可以用来终止子进程的执行。
-
timeout
number 类型,默认为 0。当改值大于 0 时,子进程的父进程会发送终止信号终止进程。
-
killSignal
string 或 integer,默认值为'SIGTERM'。
-
-
Returns
基本使用方式:
const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
ls.on('exit', (code) => {
console.log(`child process exited with code ${code}`);
});
ls.on('close', (code) => {
console.log(`child process closed with code ${code}`);
});
child_process.exec(command[, options][, callback])
创建一个 shell 然后在 shell 里执行命令。
-
command(必要参数)
string 类型,表示要执行的命令,用空格分隔。
-
options(可选)
选项 cwd、env、maxBuffer、killSignal、uid、gid、windowsHide、signal 同 spwan 一样,增加了:
-
encoding
string 类型,表示编码方式,默认为 utf8。
-
shell
string 类型,表示要执行命令的 Shell。Unix 上的默认值为
/bin/sh,Windows 上为process.env.ComSpec。 -
maxBuffer
number 类型,stdout 和 stderr 允许输出的最大的字节数(bytes),超过这个限制子进程则会被终止,所以输出都会被截断。 默认值为
1024 * 1024,也就是 1M。
-
-
callback(可选)
进程终止的回调函数,参数如下:
-
error
Error 类型,如果执行成功则为 null,如果失败返回 Error,通常来讲此时 Error.code(子进程退出的 code) 不为 0。
-
stdout
string 或 Buffer。
-
stderr
string 或 Buffer。
-
-
Returns
基本使用方式:
const { exec } = require('child_process');
//省略options
exec('ls -a', function (error, stdout, stderr) {
if (error) {
console.error(`error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
});
exec('ls -a', { cwd: '/' }, function (error, stdout, stderr) {
if (error) {
console.error(`error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
});
因为 command 的参数是以空格分隔的,因此对于一些带有空格特殊的命令需要加上引号,如下有一个叫test folder的文件夹,文件夹名中间有空格,如果不加参数 ls 会去查找./test文件夹,导致错误。
const { exec } = require('child_process');
//输出text.txt,test folder下文件
exec('ls "./test folder"',
//ls: ./test: No such file or directory
//ls: folder: No such file or directory
exec('ls ./test folder',
通过 util.promisify 可以将 exec 转换为 Promise 版本:
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const testPromiseExec = async () => {
try {
const { stdout, stderr } = await execPromise('ls "./test folder"');
console.log('testPromiseExec stdout:', stdout);
console.error('testPromiseExec stderr:', stderr);
} catch (error) {
//error多了stdout、stderr两个属性
console.error('testPromiseExec error:', error);
}
};
exec 可以通过 AbortController 来主动终止进程:
const abortExample = () => {
const controller = new AbortController();
const { signal } = controller;
const process = exec('grep history', { signal }, (error) => {
console.log(`grep ssh error:${error}`); // AbortError
});
setTimeout(() => {
controller.abort();
}, 1000);
};
abortExample();
如果执行的用户输入的命令可能带来风险,比如:
exec('rm -rf *');
child_process.execFile(file[, args][, options][, callback])
-
file
string 类型,表示要执行文件的名称或路径。
-
args
string[]类型,表示参数列表。
-
options
选项 cwd、env、encoding、timeout、maxBuffer、killSignal、uid、gid、windowsHide、signal 同 exec 一样,只新增加了一项:
-
windowsVerbatimArguments
布尔类型, 在 Windows 上不为参数加上引号或转义,默认值为 false,只在 Windows 上起作用。
-
-
callback(可选)
同 exec 一样
-
Returns
同 exec 一样
execFile 同 exec 基本一样,不同点在于 execFile 并不会衍生一个 Shell 来执行,而是直接开启新进程执行文件,这样效率会更高一些。
基本使用方式:
execFile('node', ['--version'], (error, stdout, stderr) => {
if (error) {
throw error;
}
console.log(stdout);
});
//路径
execFile(
'/Users/xinghunm/.nvm/versions/node/v16.10.0/bin/node',
['--version'],
(error, stdout, stderr) => {
if (error) {
throw error;
}
console.log(stdout);
}
);
需要注意的是 execFile 开启 Shell 选项同样会衍生 Shell
//不添加shell选项ls -a会报错
execFile('ls -a', { shell: '/bin/bash' }, function (error, stdout, stderr) {
if (error) {
throw error;
}
console.log('ls -a output:', stdout);
});
同 exec 一样都能通过 util.promisify 来使用 Promise 版本。
const { execFile } = require('child_process');
const util = require('util');
const execFilePromise = util.promisify(execFile);
const testPromiseExec = async () => {
try {
const { stdout, stderr } = await execFilePromise('node', ['--version']);
console.log('testPromiseExec stdout:', stdout);
console.error('testPromiseExec stderr:', stderr);
} catch (error) {
console.error('testPromiseExec error:', error);
}
};
testPromiseExec();
通过 AbortController 终止进程:
const controller = new AbortController();
const { signal } = controller;
const process = execFile('node', ['--version'], { signal }, (error) => {
console.log(`node --version error:${error}`); // AbortError
});
controller.abort();
child_process.fork(modulePath[, args][, options])
-
modulePath
string 类型,表示要执行的模块名字或路径。
-
args
string[]类型,表示参数列表。
-
options
选项 cwd、env、timeout、killSignal、uid、gid、windowsVerbatimArguments、signal 同 execFile 一样,其他如下:
-
detached
布尔类型, 表示子进程独立于父进程运行。
-
execPath
string 类型,表示衍生子进程的可执行文件,默认是
/usr/local/bin/node。也就是说,通过 execPath 可以指定具体的 node 可执行文件路径,比如多个 node 版本。 -
execArgv
string[]类型,表示传递给可执行文件(execPath 指定)的参数列表。默认值是 process.execArgv。
-
serialization
string 类型,表示进程间传递信息的序列化方法,可能的值是 json 和 advanced 默认值为 json。
-
silent
boolean 类型,默认是 false,如果为 true,表示静默,子进程的 stdin, stdout 以及 stderr 会直接 pipe 到父进程:
const { fork } = require('child_process'); //例1: //不会打印message from child const child = fork('./child.js', { silent: true, }); //例2: //打印出message from child const child = fork('./child.js', { silent: true, }); child.stdout.on('data', function (data) { console.log(data.toString()); }); //例3: //打印出message from child fork('./child.js', { silent: false, }); /*-------------------分割线---------------------*/ //child.j console.log('message from child'); -
stdio
<Array>或<string>类型,表示标准输入输出,如果设置了该项,silent 选项的设置会被覆盖。
-
-
callback(可选)
同 exec 一样
-
Returns
同 exec 一样
fork 是 child_process.spawn() 的一个特殊的实例,返回的ChildProcess添加了父子进程间的通讯通道:
//parent.js
const child = fork('./child.js');
child.on('message', function (message) {
//输出Receive message from child: {"from":"child"}
console.log(`Receive message from child: ${JSON.stringify(message)}`);
});
child.send({ from: 'parent' });
/*-------------------分割线---------------------*/
//child.js
process.on('message', function (message) {
//输出Receive message from parent: {"from":"parent"}
console.log(`Receive message from parent: ${JSON.stringify(message)}`);
});
process.send({ from: 'child' });
ChildProcess 事件
-
exit
close 事件的回调有两个参数:
-
code
number 类型,如果子进程自己退出,则为退出码。
-
signal
string 类型,表示终止子进程的信号。
exit 事件在子进程结束后触发,如果进程正常退出了,则 code 是进程的最终退出码,否则为 null。 如果进程是因为收到信号而终止的,则 signal 是信号的字符串名称,否则为 null。 code 和 signal 总有一个是非空的。 需要注意的是当 exit 事件触发时,子进程的 stdio 流可能依然是打开的。此外,exit(以及 close)事件并不一定触发,如下: 当子进程文件 write-to-stderr.js 通过 stderr 写入数据超过一定大小,大概在 24kb(测试环境为 24906 字节)后,进程会挂起,不会触发 exit、close 等事件。
//spawn-close-not-fire.js const { spawn } = require('child_process'); // 24576 const subProcess = spawn('node', ['write-to-stderr.js', '24906']); subProcess.on('error', function (error) { console.log(`error:${code}`); }); subProcess.on('close', function (code) { console.log(`close:${code}`); }); subProcess.on('exit', function (code) { console.log(`exit:${code}`); }); /*********************分割线*******************/ //write-to-stderr.js const count = process.argv.length > 2 ? process.argv[2] : 1000; for (let i = 0; i < count; i++) { process.stderr.write('0'); }因为少量数据系统会缓存,但是数据大了就需要客户端进行处理,上述例子中的数据超过了限值需要等待客户端处理,而主进程没有处理子进程的数据,因此子进程不能够完成,也就不能被 close。通过以下两种方式处理就能够顺利退出了。
-
方式一
消费数据:
//spawn-close-not-fire.js subProcess.stdout.on('data', function (data) { //回调代码体可以为空 console.log('stdout' + data); }); //或 subProcess.stderr.on('data', function (data) { //回调代码体可以为空 console.log('stderr' + data); }); -
方式二
设置 stdio 为 ignore,不关心输出:
//spawn-close-not-fire.js const subProcess = spawn('node', { stdio: 'ignore' }, [ 'write-to-stderr.js', '24906', ]);
-
-
close
close 事件的回调有两个参数:
-
code
number 类型,如果子进程自己退出,则为退出码。
-
signal
string 类型,表示终止子进程的信号。
close 事件在进程已结束且子进程的标准输入输出流已关闭之后触发,这与 exit 事件不同,因为多个进程可能共享相同的标准输入输出流。 close 事件始终在 exit 或 error(如果子进程衍生失败)事件后触发。
const { spawn } = require('child_process'); const ls = spawn('ls', ['-lh']); ls.on('close', (code, signal) => { console.log(`child process closed with code ${code}`); }); -
-
error
error 在以下三种情况被触发:
-
进程无法被衍生
-
进程无法被杀死
-
向子进程发送消息失败
需要注意的是 error 事件粗分后并不一定会触发 exit,当同时监听了 error 和 exit 时间时,需要防止处理函数被多次调用。
-
-
message
当子进程通过 process.send()发送消息时触发。
-
disconnect
在父进程中调用 subprocess.disconnect() 或在子进程中调用 process.disconnect() 后会触发 disconnect 事件, 断开后就不能再发送或接收信息。
-
spawn
当子进程成功衍生时触发,如果衍生失败则会触发 error 事件,spawn 事件比其他事件都要先被触发。
总结
- exec、execFile、fork 都是基于 spawn 实现
- 当想通过 Shell 语法执行命令且该命令输出的内容较小(1M 以内)时 exec 是一个好的选择
- exec 与 execFile 的区别在于 exec 会创建 Shell 来执行命令, execFile 效率会更高一些
- fork 与 spawn 的最大的区别是 fork 衍生子进程时会创建通讯通道,使得父子进程间可以发送消息
- 当预期要执行的命令输出较大时,使用 spawn 是一个好的选择
- 通过 detached 可以创建一个独立于父进程的进程