likes
comments
collection
share

child_process

作者站长头像
站长
· 阅读数 17

一、什么是进程

进程 Process

场景

  • notepad.exe 是一个程序,不是进程
  • 双击 notepad.exe时,操作系统会开启一个进程,这点可以通过任务管理器看到

定义

  • 进程是程序的执行实例
  • 程序在CPU上执行时的活动叫进程
  • 实际上并没有明确的定义,只有一些规则

特点

  • 一个进程可以创建另一个进程(父进程与子进程)
  • 通过任务管理器可以看到进程

child_process

如上图,打开chrome浏览器,它里面又有很多的子进程

了解CPU

特点

  • 一个单核的CPU,在一个时刻,只能做一件事情
  • 那么如何让用户同时看电影、听声音。写代码呢?
  • 答案是在不同的进程中快速切换(非常非常快)

多程序并发执行

  • 指多个程序在宏观上并行,微观上串行
  • 每个进程会出现『执行-暂停-执行』的规律
  • 多个进程之间会出现抢资源的现象

进程的两个状态

child_process

备注

  • 分派的意思是把cpu的空间分派给资源在cpu上运行

child_process

备注

  • 图中的小格子相当于一个进程

二、什么是阻塞进程

阻塞

等待执行的进程

  • 都是非运行态
  • 一些(A)在等待CPU资源
  • 另一些(B)在等待I/O 完成(如文件读取)
  • 如果这个时候把CPU分配给B进程,B还是在等 I/O
  • 我们把这个B叫做阻塞进程
  • 因此,分派程序只会把CPU分配给非阻塞进程

进程的是三个状态

child_process

child_process

一个进程有就绪、运行和阻塞三个状态,加上创建和终止就是五种状态

三、什么是线程

线程 Thread 的引入

分阶段

  • 在面向进程设计的系统中,进程是程序的基本执行实体
  • 在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器

引入的原因

  • 最早进程是执行的基本实体,也是资源分配的基本实体
  • 如果同时要开很多进程就会导致进程的创建、切换、销毁太消耗CPU时间了
  • 于是引入线程(小进程),线程作为执行的基本实体
  • 而进程只作为资源分配的基本实体(进程就不管执行这件事情了,只管资源分配。把内存开给进程,进程自己再开个线程去用这个内存)

备注

通过将一件事变成两件事(执行和资源分配),这样每件事处理起来就会变得更加轻量一点

线程 Thread

概念

  • CPU调度(把进程或线程从队列里拉出来,或者把进程或线程推到队列里去)和执行的最小单位
  • 一个进程中至少有一个线程,可以有多个线程
  • 一个进程中的线程共享该进程中的所有资源(内存空间、用户的存储空间,文件,鼠标,打印机等)
  • 进程的第一个线程叫做初始化线程(初始化线程可以去开其他的线程,也叫子线程)
  • 线程的调度可以由操作系统负责,也可以用户自己负责(如;用主线程去调度(优先让谁执行)其他的线程)

备注

把不相关的东西,单独放到单独的线程里,这样就不用在不同的模块中频繁的切换,线程与线程之间通过message的形式实现通信

线程是轻量级的进程,它可以共享进程中的资源

举例

  • 浏览器进程里面有渲染引擎、v8引擎、存储模块、网络模块、用户界面模块等
  • 每个模块都可以放在一个线程里

分析

  • 子进程 VS 线程 到底该用哪一个?

进程可以开启另外一个进程---> 子进程,线程也是进程开启的

优先使用线程,除非需要去单独搞一个资源分配才需要使用子进程

四、用exec创建进程

Node.js如何操作进程?

Node.js中有一个模块叫child+process 用于创建新的子进程

child_process

使用目的

  • 子进程的运行结果储存在系统的缓存中(最大200kb)
  • 等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果

使用

文档

child_process.exec它的作用是执行一个语句,一般为命令行语句

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

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

child_process

备注

在Node.js中参数回调的特点是,第一个参数是error,第二个参数是标准输出stdout,第三个参数是标准错误,也就是说如果报错,会把报错放在第一个参数,并把报错信息放在第三个参数,如果没有报错,就会把结果放到第二个参数

exec(cmd,options,fn)

  • execute:执行的缩写,用于执行bash命令
  • 同步版本:execSync (Node.js中一般不用同步用异步)

  • 返回两个流 stdoutstderr
const child_process = require('child_process')
const { exec } = child_process

const streams = exec('ls -l ../')
// 监听标准输出
streams.stdout.on('data',(chunk)=>{
    console.log(chunk)
})
// 监听错误
streams.stderr.on('data')

child_process

以上就是两种方式获取exec的操作结果

一种是使用回调

另外一种是使用它的stdoutstderr流的形式

Promise

  • 可以使其Promise化(用util.promisify)
const child_process = require('child_process')
const { exec } = child_process
const util = require('util')

const  exec2 = util.promisify(exec)

exec2('ls ../')
    .then((data)=>{
        console.log(data.stdout)
    })

child_process

有漏洞

  • 如果API中的第一个参数cmd被注入了,可能执行意外的代码
  • 推荐使用 execFile

例如:如果把下面的pwd 改为rm -rf /,结果害怕。。。

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

const  exec2 = util.promisify(exec)
const userInput = '. && pwd'

exec2(`ls ${userInput}`).then(data =>{
    console.log(data.stdout)
})

child_process

因此,exec这个API是极其危险的,不到万不得已不用,应该用execFile

五、options的取值

execFile

  • 执行特定的程序
  • 命令行的参数要用数组形式传入,无法注入
  • 同步版本: execFileSync
const child_process = require('child_process')
const { execFile } = child_process

const userInput = '. && pwd'

execFile('ls',['-la',userInput],(error,stdout)=>{
    console.log(stdout)
    console.log(error)
})

child_process

备注

不会把&&当做连接号而是当做路径了

它是通过数组的形式传入后面的每一个部分,因此没有办法再传入&&,所有的东西都会被当做参数

支持流吗?试一试!

const child_process = require('child_process')
const { execFile } = child_process

const userInput = '.'

const streams = execFile('ls',['-la',userInput])
streams.stdout.on('data',(chunk)=>{
    console.log(chunk)
})

child_process

因此,execFile也是支持流的!!

options

几个常用的选项

  • cwd - 默认是当前目录,你也可以改到其他目录
  • env - 环境变量
  • shell - 用什么shell
  • maxBuffer - 最大缓存,默认 1024 * 1024 字节(使用回调的形式有限制,使用流的形式没有限制)
const child_process = require('child_process')
const { execFile } = child_process

const userInput = '.'

execFile('ls',['-la',userInput],{
    cwd: 'C:\'
},(error,stdout)=>{
    console.log(error)
    console.log(stdout)
})

child_process

这时候打印的就不是当前目录的目录结构了,而是c盘的目录结构了

关于maxBuffer

回调形式的execFile会先将 ls 命令执行完了,再去调回调(除非用的是流的形式,流的形式是只要有结果就告诉我们)

这个过程中结果是存在一个Buffer内存里的,这个内存有多大呢?就是通过这个options去设置

六、Node.js的execFile、spawn和fork

spawn

用法

  • 用法与execFile方法类似
  • 没有回调函数,只能通过流事件获取结果
  • 没有最大200kb的限制(因为是流)
const child_process = require('child_process')
const { spawn } = child_process

const userInput = '.'

const streams = spawn('ls',['-la',userInput],{
    cwd: 'C:\'
})

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

child_process

经验

  • 能用spawn的时候就不要用 execFile,因为如果使用execFile中回调的形式,就会有最大200kb的限制

fork

实际上是一个语法糖

实际开发中用的最多的还是fork因为我们经常使用的是JS脚本,而不是bash脚本

fork(执行Node.js的程序) > spawn(执行任意的程序) > execFile > exec

用法

  • 创建一个子进程,只能执行Node脚本
  • fork('./child.js') 相当于 spawn('node',['./child.js'])

fork会启动这个child.jsjs文件,去执行它, 执行的时候会新创建一个子进程

特点

  • 会多出一个message事件,用于父子通信
  • 会多出一个send方法

代码

新建n.js - 主进程

const child_process = require('child_process')
// 使用child.js创建子进程
var n = child_process.fork('./child.js')

n.on('message',function (m){
    console.log('父进程得到值',m)
})

新建child.js - 子进程

setTimeout(()=>{
    process.send({foo: 'bar'})
},2000)

child_process

这样父进程就得到了子进程发送过来的数据了

那么,子进程能不能拿到父进程发送过来的数据?

// n.js
const child_process = require('child_process')
// 使用child.js创建子进程
var n = child_process.fork('./child.js')

n.on('message',function (m){
    console.log('父进程得到值',m)
})

n.send({hello:'world'})

// child.js
process.on('message',function (m){
    console.log('子进程得到了主进程传过来的数据',m)
})
setTimeout(()=>{
    process.send({foo: 'bar'})
},2000)

child_process

由于它们互相都在监听对方的messge,父进程等子进程,子进程等主进程的情况。最好不要出现这种情况,容易造成进程不退出

Node.js就一直在那等,没有退出

以上就是使用fork创建一个Node.js的子进程,并且子进程和父进程互相通信

七、Node.js操作线程

一些历史

child_process.exec

  • v0.1.90 进入 Node.js

new Worker(创建线程)

  • v10.5.0 加入Node.js,由于加入的时间比较晚, 所以一些框架和库都没有使用,因此我们最好也少用
  • v11.7.0 之前需要 -- experimental-worker 开启

这个线程API太新了

  • 所以我们应该不会经常用到

效率

目前效率并不够高,文档自己都写了

工作线程对于执行 CPU 密集型的 JavaScript 操作很有用。 它们对 I/O 密集型的工作帮助不大。 Node.js 内置的异步 I/O 操作比工作线程更高效。

备注

  • CPU密集型操作指的是,如: 加密、解密、加减乘除尤其是除

  • I/O密集型操作值的是:比如:访问数据库,数据库就是一个I/O,因为数据库是通过网路访问的,MySQL是一个Server一个Client,它们之间通过一个mysql协议进行网络访问,网络实际上就是一个输入输出

API列表

  • isMainThread
  • new Worker(filename): 创建线程,其中filname这个文件就是这个线程要做的事
  • parentPort 是用来做线程之间通信的

备注

一个进程开启的时候就要开启一个初始化的线程,那个线程就是主线程

事件列表

  • message: 获取到的信息
  • exit: 知道线程中断了

如果对进程、线程感兴趣可以学习一本关于操作系统的书

转载自:https://juejin.cn/post/7252685795084468284
评论
请登录