开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14天,点击查看活动详情
前言
what is process?
我们启动一个服务、运行一个实例,就是开一个服务进程,例如 Java 里的 JVM 本身就是一个进程,Node.js 里通过 node app.js 开启一个服务进程,
what is multi_process?
多进程就是进程的复制(fork),fork 出来的每个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了 IPC 通信,进程之间才可数据共享。
when do we need child_process?
node是单进程的,这个我们都知道,但是一个 CPU 一个进程不足以处理程序中日益增加的工作负载。所以为了充分利用我们的cpu的处理能力, 快速处理任务,我们可以利用子进程。
进程的概念主要有两点
- 在操作系统中,每个进程都有自己的内存空间、代码、数据、打开的文件等资源,进程与进程之间相互独立,互不干扰。
- 进程是一个"执行中的程序",存在嵌套关系。
node的child_process
在大多数情况下,NodeJs是不需要并发执行的,因为它是事件驱动性永不阻塞。
但是单进程存在一个问题,就是无法充分利用cpu等资源。
NodeJs提供了child_process模块来实现子进程,从而实现一个node多进程。
通过child_process模块,可以实现1个主进程,多个子进程的模式,主进程称为master进程,子进程又称为工作进程。
child_process模块中包括了很多创建子进程的方法,包括fork、spawn、exec、execFile等等。
spawn
在这4个API中以spawn最为基础,因为其他三个API或多或少都是借助spawn实现的。所以我们先来讲spawn
spawn的语法如下
child_process.spawn(command[, args][, options])
ChildProcess继承EventEmitter
spawn是没有回调函数的,所以我们需要接受spawn返回的实例。
因为ChildProcess类是继承过EventEmitter的,所以可以在子进程的实例上添加事件的回调函数,常用的事件包括
- exit:该时间在子进程结束后触发
- error: 子进程发生错误后触发,此时exit有可能触发也有可能不触发
每个子进程具有标准流
每个子进程具有标准的stdio.所以我们可以使用child.stdin, child.stdout, child.stderr
当标准流stdio关闭时,会触发close事件。close事件和exit事件不同,因为多个子进程共享同一个标准流stdio,所以当一个子进程退出,并不代表标准流会关闭,也就是 exit事件被触发的时候,close 事件不一定被触发。
由于所有流(Stream)都继承了EventEmitter,所以,可以在stdio附加到每个子进程的流上监听不同的事件。与正常进程不同的是,在子进程中,stdout/stderr是可读流,而stdin流是可写流。这和主进程截然相关。最重要的是,在可读流上,我们可以监听data事件,该事件将在命令输出或执行命令时遇到的任何错误的时候,都会触发data 事件
const cp = require('child_process');
const child = cp.spawn('ls', ['-al']);
child.stdout.on('data', (data) => {
console.log(data.toString());
})
执行结果如下
drwxr-xr-x 3 wson staff 96 Feb 18 20:45 .
drwx------@ 87 wson staff 2784 Feb 18 20:45 ..
-rw-r--r-- 1 wson staff 516 Feb 18 21:24 index.js
常用的options
常用的
options主要是cwd和stdio
- cwd: 当前的子进程工作目录,比如我们npm install后,安装到当前的工作目录,就可以写成
cwd: process.cwd() - stdio: 就是标准输入输出留的是指,我们常设置值为'inherit',这将允许这个子进程阅读父进程的输入,以及写出到父进程,父进程和子进程将会使用相同的命令行
exec: 执行shell脚本命令
exec的语法如下
child_process.exec(command[, options][, callback])
从语法中我们可以看出,命令行上的参数是和命令行在一起的,所以command上的命令可以写的很长
我们在当前目录下建一个名称为你好的txt文件
const cp = require('child_process');
cp.exec('ls -al | grep "你好"', (error, stdout) => {
console.log(stdout);
})
就可以搜索到了
execFile: 执行shell脚本文件
child_process.execFile(file[, args][, options][, callback])
execFile 用来执行一个shell脚本文件。
ls -al | grep package.json
cp.execFile(
path.resolve(__dirname, 'test.shell'),
[],
function (err, stdout) {
console.log('stdout', stdout)
}
)
不过execFile也能执行命令,不过配置必须放在第二个参数中,第一个参数只能是命令。
cp.execFile('ls', ['-al'], function (err, stdout, stderr) {
console.log('err', err)
console.log('stdout', stdout)
console.log('stderr', stderr)
})
其实 execFile('ls')里面的ls其实也是一个文件,这个文件是/bin/ls
spawn与exec的相同点
1、都用于开一个子进程执行指定命令。
2、都可以自定义子进程的运行环境。
3、都返回一个ChildProcess对象,所以他们都可以取得子进程的标准输入流、标准输出流和标准错误流。
spawn与exec的不同点
1、接受参数的方式:spawn使用了参数数组,而exec则直接接在命令后。
比如要运行 du -sh /disk1 命令, 使用spawn函数需要写成spawn('du', ['-sh ', '/disk1']),而使用exec函数时,可以直接写成exec('du -sh /disk1')。exec是会先进行Shell语法解析,因此用exec函数可以更方便的使用复杂的Shell命令,包括管道、重定向等。
2、子进程返回给Node的数据量:spawn没有限制子进程可以返回给Node的数据大小,而exec则在options配置对象中有maxBuffer参数限制,且默认为200K,如果超出,那么子进程将会被杀死,并报错:Error:maxBuffer exceeded,虽然可以手动调大maxBuffer参数,但是并不被推荐。由此可窥见一番Node.js设置这两个API时的部分本意,spawn应用来运行返回大量数据的子进程,如图像处理,文件读取等。而exec则应用来运行只返回少量返回值的子进程,如只返回一个状态码。
3、exec方法相比spawn方法,多提供了一个回调函数,可以更便捷得获取子进程输出。这与从返回的ChildProcess对象的stdout或stderr监听data事件来获得输出的区别是: data事件的方式,会在子进程一有数据时就触发,并把数据返回给Node。而回调函数,则会先将数据缓存在内存中(数据量小于maxBuffer参数),等待子进程运行完毕后,再调用回调函数,并把最终数据交给回调函数。
fork
fork的第一个参数是一个文件的路径。 fork的语法如下
child_process.fork(modulePath[, args][, options])
// index.js
cp.fork(path.resolve(__dirname, 'child.js'))
我们可以看到fork的作用其实类似于require的作用,就是执行了第一个参数传递的文件。但是与require不同的是fork会创建一个新的node进程,然后启动一个独立的v8引擎去执行这个文件。即两个进程之间是一个完全独立的关系。