《node-child_process》

152 阅读7分钟

一. 进程基本概念

以Chrome浏览器为例,当双击打开浏览器,操作系统就开启了一个进程。一个进程可以创建另一个进程。比如Chrome浏览器第一次打开的是一个主进程,这个主进程又会开启其他子进程。 我们双击打开一个程序,CPU给这个程序分配了资源,内存等,这个程序就执行了,这就是一个进程。

以一个单核CPU为例,它在一个时刻,只能做一件事。那用户是怎么同时看电影,写代码的呢?

这是因为CPU会在不同进程之间来回切换。所以用户尽管打开了多个程序,可以一起做很多事情,但其实这些程序是被不断切换的。也就是说,每个进程都是处于【执行-暂停-执行】这样的循环的,为什么会暂停呢?就是因为被其他进程抢占了资源,CPU不能分配了,于是就先暂停一会。

所以进程有两种状态:运行 非运行 当双击打开一个程序,就创建了一个进程。这个进程首先处于非运行状态,因为不知道CPU是否空闲。如果CPU空闲了,就由一个分派程序给进程分配资源,于是进入运行状态。如果运行完了,就正常退出。如果在运行过程中,资源被其他进程抢占了,CPU需要暂停本进程,切换到其他进程。于是就进入非运行状态等待。

处于非运行状态的进程,当等到了CPU空闲,就会被执行。但是如果它此时正在等待I/O的结果,那即使CPU空闲,轮到它了,它也会占着茅坑不拉屎。显然这是不高效的。

所以,分派程序起到了作用。它只会把CPU分配给目前没事做的进程。

真正在等待CPU资源的我们叫做非阻塞进程

在等待比如I/O结果的我们叫做阻塞进程

所以处于非运行状态的进程,详细划分,还可分为阻塞和非阻塞进程。这时进程就有三种状态了:运行态,阻塞态(目前正在做其他事),就绪态(CPU来了我马上就能利用) 如果它正在做一些需要等待的事件,就进入阻塞态,等到事件完成了,才会进入就绪状态,才有机会被分到CPU

二. 线程出现了

在没有线程的时候,进程既是程序执行的基本单位,也是资源分配的基本单位。执行一个程序,开一个进程,没有再小的执行单元了。

但是这样的问题就是,CPU需要在不同的进程之间切换,而且进程的创建,销毁也很浪费资源。

所以引入了更轻量的线程,让线程成为程序执行的基本单元。进程只是资源分配的基本单位。CPU把资源分配给进程,其余事情就不管了。程序的真正执行由进程里边的一个个线程去做,由进程再把资源分给这些线程

三 . node js控制进程

node js里边的child_process子进程模块用来控制进程。 如果它新建了子进程,这个子进程的运行结果会储存在系统缓存里,子进程运行结束后,再由主进程用回调读取结果。

它有以下四个API:

1. exec 创建进程

exec(cmd,options,callback)  //返回一个ChildProcess实例

衍生 shell 并且在 shell 中运行命令,当完成时则将 stdout 和 stderr 传给回调函数。

const {exec}=require('child_process')

exec('ls ../',(error,stdout,stderr)=>{
    console.log(error)
    console.log(stdout)
    console.log(stderr)
})

exec方法接受的第一个参数是字符串,是要执行的命令。第二个参数是回调,执行完命令后被调用,并且传进三个参数。如果执行成功,error就为null。并且stdout就是执行结果。

回调函数可以使用流代替:exec方法返回的对象的stdout和stderr都是流,可以监听他们的data事件来得到数据。

const {exec}=require('child_process')

const streams=exec('ls ../')
streams.stdout.on('data',(chunk)=>{
    console.log('stdout:'+chunk)
})
streams.stderr.on('data',chunk=>{
    console.log('stderr:'+chunk)
})

util.promisify

使用方法:传入一个遵循常见的错误优先回调风格的函数(即以 (err, value) => ... 回调作为最后一个参数),并返回一个返回 promise 的版本。即original必须是一个函数,而且最后一个参数是回调,这个回调的第一个参数是error。

util.promisify(original)

上边exec最后一个参数是回调,我们用promise代替:

const {exec}=require('child_process')
const util=require('util')
// 包装成promise
const exec2=util.promisify(exec)
exec2('ls ../').then((data)=>{
    console.log(data.stdout)
})

如果命令执行成功了,打印执行结果的stdout选项。

如果我只打印出data,发现是一个对象:有stdout和stderr属性。

{
  stdout: 'account_r\n' +
    'accounting\n' +
    'active-server\n' +
    'ajax-1\n' +
    .................
    '哆啦A梦\n' +
    '皮卡丘项目\n',
  stderr: ''
}

exec的漏洞

const {exec}=require('child_process')
const util=require('util')

const exec2=util.promisify(exec)
const userInput='. && pwd'
exec2(`ls ${userInput}`).then((data)=>{
    console.log(data.stdout)
})

如果cmd被用户注入,比如我本来想让用户输入一段目录地址,但是如果用户输入的是上边的例子,那就是先ls .列出当前目录的文件,然后pwd。执行结果是这样的:pwd执行成功了。那么用户就可以注入任何恶意代码了。这很危险。

$ node 1.js
1.js
/d/jirengu/node-child-process-1

代替地要使用execFile

2. execFile

execFile(file,args,options,callback)

file : <string> 要运行的可执行文件的名称或路径。

args: <string>[] 字符串参数的列表

和exec不同的是,execFile的第一个参数只能是单个的命令,后边的参数要放在第二个参数,字符串数组里。

const {execFile}=require('child_process')

const userInput='. && pwd'
execFile('ls',[userInput],(error,stdout,stderr)=>{
    console.log(error)
     console.log(stdout)
 })

执行后得到的结果:

Error: Command failed: ls . && pwd
ls: cannot access '. && pwd': No such file or directory

它是只会把数组里的参数当作路径,而不会解析出 &&

这两个API的倒数第二个参数都是options,是一个对象,有几个常用的属性:

  1. cwd Current Working Directory 子进程的当前工作目录
    const userInput='.'
    execFile('ls',[userInput],{cwd:'C:\\'},(error,stdout,stderr)=>{
    	console.log(error)
     	console.log(stdout)
    })
    
    本来是执行ls .,设置了cwd属性后,就变成了ls c:\列出c盘下的文件
  2. env 环境变量
  3. shell 指定用什么shell,我们默认就是bash,用git bash执行命令。

3. spawn

spawn(cmd,args,options)

使用给定的 command 创建新的进程,并传入 args 中的命令行参数。返回 ChildProcess 对象

与execFile的用法类似,只是没有回调。只能通过事件流得到结果。

const {spawn}=require('child_process')
const userInput='.'
const stream=spawn('ls',[userInput],{
    cwd:'C:\\'
    })

stream.stdout.on('data',chunk=>{
    console.log(chunk.toString())
})

经验:能用spawn的时候,就不要用execFile 因为spawn是流,没有内存限制

4. fork

fork(modulePath[, args][, options]) // 返回一个ChildProcess 对象

modulePath :<string> 要在子进程中运行的模块。

fork 用于创建一个执行node脚本的子进程。spawn的区别:spawn也是创建新的子进程,但是可以执行任何程序。而fork相当于spawn的特例,这个子进程只能执行node脚本。

尽量用fork,而不是spawn

即:fork('./child.js') 意思是创建一个用node执行child.js的子进程,它相当于:spawn('node',['./child.js']) 用spawn执行node child.js

特点: 执行fork后,返回的 ChildProcess 对象会内置额外的通信通道(在父进程与子进程之间建立了一个IPC通道),允许消息在父进程和子进程之间来回传递。

// n.js
const {fork}=require('child_process')

const n=fork('./child.js')
n.on('message',(data)=>{
    console.log(`父进程接收到值:`)
    console.log(data)
})
n.send({hello:'world'})

在n.js里,fork一个文件,意思就是创建一个子进程用来执行这个node文件。fork后,就有了通信通道。我们监听父进程的message事件,打印出收到的数据。同时也通过send()方法,向子进程发送消息。

// child.js
setTimeout(()=>{
    process.send({'foo':'bar'})
},2000)
process.on('message',(data)=>{
    console.log('子进程接收到值:',data)
})

这个被执行的子进程里,两秒后 通过process.send() 向父进程发消息。为了收到父进程传来的消息,也监听message事件。

执行node n.js ,看到以下结果:

$ node n.js
子进程接收到值: { hello: 'world' }
父进程接收到值:
{ foo: 'bar' }