1 进程

1.1 进程 Process

  • 场景
    • notepad.exe是一个程序,不是进程
    • 双击notepad.exe时,操作系统会开启一个进程
  • 定义
    • 进程是程序的执行实例
    • 程序在CPU上执行时的活动叫做进程
  • 特点
    • 一个进程可以创建另一个进程(父进程和子进程)例如Chrome浏览器的网页和插件
    • 通过任务管理器可以看到进程

1.2 了解CPU

  • 特点:

    • 一个单核CPU,在一个时刻,只能做一件事情
    • 如何让用户同时看电影、听音乐、写代码呢
    • 答案是在不同进程中快速切换(CPU频率)
    • 以渣男交多个女朋友举例
  • 多程序并发执行时

    • 多个程序在宏观上并行,微观上串行
    • 每个进程会出现执行-暂停-执行的规律
    • 多个进程之前会出现抢资源(如打印机队列)的现象
  • 进程的两个状态:运行态与非运行态

    image-20210805104328732

  • 进程队列示意

    image-20210805104451902

1.3 阻塞

  • 等待执行的进程中

    • 都是非运行态
    • 一些(A)在等待CPU资源
    • 另一些(B)在等待I/O完成(如文件读取)
    • 如果这时把CPU资源分配给B进程,B还是在等I/O(不占用CPU)
    • B进程就是阻塞进程
    • 因此,分派程序只会把CPU分配给非阻塞进程
    • 比如和女朋友排队买饭,但她中途去厕所了,那么如果排到你们了就会让你去后面重新排队
  • 进程的三个状态:运行就绪阻塞

    image-20210805105749846

2 线程

2.1 线程引入

  • 分阶段
    • 过去在面向进程设计的系统中,进程是程序的基本执行实体
    • 在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器
  • 引入原因
    • 进程是程序的基本执行实体,也是资源(内存等)分配的基本实体
    • 导致进程的创建、切换、销毁太消耗CPU时间了
    • 于是引入线程,线程作为执行的基本实体
    • 进程只作为资源分配的基本实体(不管执行了)
    • 例如领导与员工们,领导负责申请、调配资源,员工们负责执行

2.2 线程Thread

  • 概念
    • CPU调度和执行最小单元
    • 一个进程中至少有一个线程
    • 一个进程中的线程共享该进程的所有资源
    • 进程的第一个线程叫做初始化线程
    • 线程的调度可以由操作系统负责,也可以由用户自己负责
  • 举例
    • 浏览器进程中有渲染引擎、V8引擎、存储模块、网络模块、用户界面模块等
    • 每个模块都可以放在一个线程里
  • 分析:子进程 VS 线程
    • 优先使用线程,除非需要新的资源分配

3 Node.js进程控制

3.1 child_process模块

  • 目的

    • 子进程的运行结果存储在系统缓存中(最大200kb)

    • 等待子进程运行结束后,主进程再用回调函数读取子进程的运行结果(error,stdout,stderr)

      1
      2
      3
      4
      5
      6
      7
      8
      
      const child_process = require('child_process')
      const {exec} = child_process
      
      exec('ls', (error, stdout, stderr)=>{
      	console.log(error) // null
      	console.log(stdout) // 标准输出
      	console.log(stderr) // 错误内容
      })
      

3.2 API

  • API-exec

    • exec(cmd, options, fn)

      • execute的缩写,用于执行bash命令
      • 同步版本:execSync,不用回调,直接const result = execSync
      • 返回流(不用回调,可以监听stdout流的data事件)

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        
        const child_process = require('child_process')
        const {exec} = child_process
        
        const child = exec('ls -l')
        // 监听stdout的data事件
        child.stdout.on('data', (chunk) => {
        	console.log('得到了一次数据')
        	console.log(chunk)
        })
        // 监听stderr
        child.stderr.on('data')
        
    • Promise

      • 可以使其Promise化(util.promisify)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        
        const util = require('util')
        const child_process = require('child_process')
        const { exec } = child_process
        
        const exec2 = util.promisify(exec)
        
        exec2('ls -l').then(data => {
        	console.log(data.stdout)
        })
        
    • options常用选项(exec第二个参数,execFile第三个参数)

      • cwd:执行命令的路径
      • env:环境变量
      • shell:用什么命令行程序执行
      • maxBuffer:最大缓存
      1
      2
      3
      4
      5
      
      exec('ls', {
      	cwd : 'C:\\',
      	env : {NODE_ENV : 'development'},
      	maxBuffer : 1024*2024
      })
      
    • 存在的问题

      • 存在cmd被注入的风险
      1
      2
      3
      4
      
      // 当接受用户输入时,以'&&'开头,接着输入一条危险指令
      const userInput = '. && rm -rf /'
      exec2(`ls ${userInput}`).then(data)...
      // 那么命令行就会执行'ls'命令和'rm -rf /'命令
      
      • 推荐使用execFile

        • 执行特定的程序

        • 命令行的参数要用数组形式传入,无法注入

        • 同步版本:execFileSync

          1
          2
          3
          4
          5
          6
          7
          8
          
          const { execFile } = child_process
          
          const userInput = '. && pwd'
          execFile('ls', ['-la', userInput], (error, stdout) => {
              console.log(error)
              console.log(stdout)
          })
          // 结果会报错,不会将'&&'作为连接号解析
          
        • 支持流

          1
          2
          3
          4
          
          const child = execFile('ls', [userInput])
          child.stdout.on('data', chunk => {
              console.log(chunk)
          })
          
  • API-spawn

    • spawn

      • 用法与execFile方法类似
      • 区别在于没有回调函数,只能通过流事件获取结果
      • 没有最大200kb的限制(因为是流)
      1
      2
      3
      4
      
      const child = spawn('ls', ['.'], { cwd : 'C:\\' })
      child.stdout.on('data', chunk => {
          console.log(chunk.toString())
      })
      
    • 能用spawn就不要用execFile

  • API-fork

    • fork

      • 创建一个子进程,执行Node脚本
      • fork('./child.js')相当于执行spawn(‘node’,['./child.js'])
      • 能用fork,就别用其他的了
    • 特点

      • 会多出一个message事件,用于父子通信
      • 会多出一个send方法
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      // n.js主进程
      var n = child_process.fork('./child.js')
      n.on('message', m => {
      	console.log('PARENT got message:', m)
      })
      n.send({ hello: 'world' })// 父进程给子进程传值
      
      // child.js子进程
      process.on('message', m => {
      	console.log('CHILD got message:', m)
      })
      process.send({ foo: 'bar' })// 子进程给父进程传值
      

4 Node.js线程

太新了 官方文档建议用子进程。。

https://nodejs.org/api/worker_threads.html#worker_threads_worker_threads