创建和控制外部进程
Node是被设计用来高效处理/O操作的,但正如你所见,某些类型的程序并不适用于这种模式。比如当用Node处理一个CPU密集型任务时可能会阻塞事件循环,所以应该将CPU密集任务分配给另一个进程去处理,释放事件循环,分担主进程的压力。
Node中可以创建进程,并把这些进程当成当前启动他的进程的子进程。
也许你不是很了解进程和线程,但这不是本文的重点,所以如果想要了解的更清楚,请查一查,简单了解一下。
本文的内容在于:
- 如果生成外部命令
- 创建子进程
- 通信
- 终止
- 在Node进程之外实现多任务操作
提一句,后面用到的process对象是Node中的一个全局对象。
执行外部命令
使用child_process
模块来执行外部的shell命令或者可执行文件。
使用exec
函数:
const child_process = require('child_process');
child_process.exec("命令","可选参数","回调");
第一个参数为字符串类型的待执行的shell命令,第二参数是回调。回调应该有三个参数:error、stdout、stderr。如果出现错误,第一个参数接收,如果第一个参数不包含错误,第二个参数接收命令的的输出信息,最后一个包含命令的错误输出信息。
三个参数为啥叫这个名字: std => standard ; out => output ; err => error
可选参数是一个可传可不传的对象,他可以包含很多配置属性,比如超时时间、当前工作的目录(如果想命令到指定地方工作)、编码格式、环境变量等等等等,很多,可以自己查阅文档。例如,也许你想给子进程提供一组环境变量,作为父进程环境变量的扩展,如果直接修改process.env,就会导致每个模块都被修改,所以你可以复制process.env到一个本地变量,然后进行修改,最后把这个本地变量传入你的进程。
可以在同一个目录创建两个文件,par.js
和child.js
,通过前者启动后者。
// 这是par.js里面的代码
const exec = require('child_process').exec;
exec('node child.js' ,{env: { number: 123 }},function(err ,stdout ,stderr) {
if(err) return;
console.log(stdout);
console.log(stderr);
})
// 这是child.js中的代码
const number = process.env.number;
console.log(number); // 输出一个字符串
结合上面的介绍体会体会。
生成子进程
使用exec函数启动外部进程,并且在结束时回调一个函数,不过存在缺点:
- 除了命令行参数和环境变量,没有通信手段。
- 子进程的输出被缓存,导致无法对其进行流操作,可能导致内存耗尽。
** 不过child_process
可以实现更精细的工作。上面的例子是执行了一个外部进程,这个进程是外部的,不是父进程创建的,而是启动的。所以通过创建一个子进程,我们可以获取更多的操作权限。
创建子进程
const spawn = require("child_process").spawn;
const child = spawn("tail",['-f', '/var/log/system.log']);
我们创建了一个子进程,依据传递中括号中的两个参数执行tail
命令,tail
会监视路径标识的那个文件,将输出的数据加载到stdout流。spawn会返回一个ChildProcess
对象,他是一个句柄,封装了对实际进程的访问。
句柄:一种特殊的智能指针,当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。句柄与普通指针的区别在于,指针包含的是引用对象的内存地址,而句柄则是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。这种间接访问对象的模式增强了系统对引用对象的控制。
监听子进程的输出数据
spawn
返回的对象都具有一个 stdout
属性,它以流的形式表示标准输出信息,可以在其上绑定事件监听:
child.stdout.on('data', function(data){
console.log(data);
})
每当子进程将数据标准输出的时候,父进程就会触发事件,调用回调函数打印数据。
除了标准输出之外,还有一个默认输出流(standard error),通常用于输出错误消息。
例如,文件不存在,那么tail进程就会报错没有该文件。父进程通过监听stderr流获取通知:
chlid.stderr.on('data', function(data){
console.log("error: " + data);
})
向子进程发送信息
标准输入流使用childProcess.stdin
。子进程也可以使用process.stdin
监听数据。
默认process.stdin是暂停状态,所以先要恢复他
stdin => standard input
子进程程序(C.js):
// 恢复标准输入流
process.stdin.resume();
process.stdin.on('data', function(data) {
console.log(data);
// 输出
process.stdout.write("childe data : " + data);
})
如果你执行运行这个程序,那么在窗口中会等待你输入一个值,按下回车之后会在屏幕上看到输出值。
父进程程序(P.js):
// 使用node进程创建一个子进程执行子进程程序
const child = spawn('node', ['C.js']); // 中括号中可只填写一个路径,此时两个js文件在同一目录下,加上node拼接成一句完整命令
// 随便发送一个数据到子进程
child.stdin.write("childe");
child.stdout.once('data', function(data){
console.log("子进程传递过来的数据/n");
console.log(data);
})
为什么要是用noce?如果子进程在不同时间多次发送数据,我们想获取所有数据,可以设置一个定时器,如果在定时器中使用
on
绑定事件,那么随着时间推移将要注册多个回调函数(回到函数不是覆盖而是追加!因为并没有清楚之前的回调函数!),这样每次触发事件,所有回调都会被调用。所以使用once是个良好的习惯。
当子进程退出时获得通知
child.on("exit", function(code) {
// code: 子进程终止的退出码,非0则是不正常退出
console.log(code);
})
退出之后,会自动打印终止的有关信息。
如果子进程是被一个信号终止的(kill函数,看下面),那么相应的信号会作为第二个参数传递给回调。
终止子进程
信号是父子通信的简单方式,还可以用来终止进程。
一般可以使用child.kill
方法向子进程发送一个信号,默认发送的是’SIGTERM’,还可以手动传递一个信号给kill函数。
尽管方法叫kill,但是发送的信息却不一定会终止进程!
子进程可以定义程序重写信号默认行为:
process.on('SIGUSR2', function() {
...
})
SIGKILL、SIGSTOP比较特殊,无法重写默认行为。