一、什么是进程
进程 Process
场景
- notepad.exe 是一个程序,不是进程
- 双击 notepad.exe时,操作系统会开启一个进程,这点可以通过任务管理器看到
定义
- 进程是程序的执行实例
- 程序在CPU上执行时的活动叫进程
- 实际上并没有明确的定义,只有一些规则
特点
- 一个进程可以创建另一个进程(父进程与子进程)
- 通过任务管理器可以看到进程
如上图,打开chrome浏览器,它里面又有很多的子进程
了解CPU
特点
- 一个单核的CPU,在一个时刻,只能做一件事情
- 那么如何让用户同时看电影、听声音。写代码呢?
- 答案是在不同的进程中快速切换(非常非常快)
多程序并发执行
- 指多个程序在宏观上并行,微观上串行
- 每个进程会出现『执行-暂停-执行』的规律
- 多个进程之间会出现抢资源的现象
进程的两个状态
备注
- 分派的意思是把cpu的空间分派给资源在cpu上运行
备注
- 图中的小格子相当于一个进程
二、什么是阻塞进程
阻塞
等待执行的进程
- 都是非运行态
- 一些(A)在等待CPU资源
- 另一些(B)在等待I/O 完成(如文件读取)
- 如果这个时候把CPU分配给B进程,B还是在等 I/O
- 我们把这个B叫做阻塞进程
- 因此,分派程序只会把CPU分配给非阻塞进程
进程的是三个状态
一个进程有就绪、运行和阻塞三个状态,加上创建和终止就是五种状态
三、什么是线程
线程 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)
})
备注
在Node.js中参数回调的特点是,第一个参数是error,第二个参数是标准输出stdout
,第三个参数是标准错误,也就是说如果报错,会把报错放在第一个参数,并把报错信息放在第三个参数,如果没有报错,就会把结果放到第二个参数
exec(cmd,options,fn)
- execute:执行的缩写,用于执行bash命令
- 同步版本:execSync (Node.js中一般不用同步用异步)
流
- 返回两个流
stdout
和stderr
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')
以上就是两种方式获取exec的操作结果
一种是使用回调
另外一种是使用它的stdout
和stderr
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)
})
有漏洞
- 如果API中的第一个参数cmd被注入了,可能执行意外的代码
- 推荐使用 execFile
例如:如果把下面的pwd
改为rm -rf /
,结果害怕。。。
const { exec } = child_process
const util = require('util')
const exec2 = util.promisify(exec)
const userInput = '. && pwd'
exec2('ls ../')
.then((data)=>{
console.log(data.stdout)
})
因此,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)
})
备注
不会把&&
当做连接号而是当做路径了
它是通过数组的形式传入后面的每一个部分,因此没有办法再传入&&
,所有的东西都会被当做参数
支持流吗?试一试!
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)
})
因此,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)
})
这时候打印的就不是当前目录的目录结构了,而是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())
})
经验
- 能用spawn的时候就不要用 execFile,因为如果使用execFile中回调的形式,就会有最大200kb的限制
fork
实际上是一个语法糖
实际开发中用的最多的还是fork
因为我们经常使用的是JS脚本,而不是bash脚本
fork(执行Node.js的程序) > spawn(执行任意的程序) > execFile > exec
用法
- 创建一个子进程,只能执行Node脚本
fork('./child.js')
相当于spawn('node',['./child.js'])
fork会启动这个child.js
js文件,去执行它, 执行的时候会新创建一个子进程
特点
- 会多出一个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)
这样父进程就得到了子进程发送过来的数据了
那么,子进程能不能拿到父进程发送过来的数据?
// 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)
由于它们互相都在监听对方的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: 知道线程中断了
如果对进程、线程感兴趣可以学习一本关于操作系统的书