前端不懂进程通讯?看完这篇就懂了

为何前端要了解进程通讯:javascript

前端领域已经不是单纯写在浏览器里跑的页面就能够了,还要会 electron、nodejs 等,而这俩技术都须要掌握进程通讯。html

nodejs 是 js 的一个运行时,和浏览器不一样,它扩展了不少封装操做系统能力的 api,其中就包括进程、线程相关 api,而学习进程 api 就要学习进程之间的通讯机制。前端

electron 是基于 chromium 和 nodejs 的桌面端开发方案,它的架构是一个主进程,多个渲染进程,这两种进程之间也须要通讯,要学习 electron 的进程通讯机制。java

这篇文章咱们就来深刻了解一下进程通讯。node

本文会讲解如下知识点:c++

  • 进程是什么
  • 本地进程通讯的四种方式
  • ipc、lpc、rpc 都是什么
  • electron 如何作进程通讯
  • nodejs 的 child_process 和 cluster 如何作进程通讯
  • 进程通讯的本质

进程

咱们写完的代码要在操做系统之上跑,操做系统为了更好的利用硬件资源,支持了多个程序的并发和硬件资源的分配,分配的单位就是进程,这个进程就是程序的执行过程。好比记录程序执行到哪一步了,申请了哪些硬件资源、占用了什么端口等。git

进程包括要执行的代码、代码操做的数据,以及进程控制块 PCB(Processing Control Block),由于程序就是代码在数据集上的执行过程,而执行过程的状态和申请的资源须要记录在一个数据结构(PCB)里。因此进程由代码、数据、PCB 组成。github

pcb 中记录着 pid、执行到的代码地址、进程的状态(阻塞、运行、就绪等)以及用于通讯的信号量、管道、消息队列等数据结构。web

进程从建立到代码不断的执行,到申请硬件资源(内存、硬盘文件、网络等),中间还可能会阻塞,最终执行完会销毁进程。这是一个进程的生命周期。shell

进程对申请来的资源是独占式的,每一个进程都只能访问本身的资源,那进程之间怎么通讯呢?

进程通讯

不一样进程之间由于可用的内存不一样,因此要经过一个中间介质通讯。

信号量

若是是简单的标记,经过一个数字来表示,放在 PCB 的一个属性里,这叫作信号量,好比锁的实现就能够经过信号量。

这种信号量的思想咱们写前端代码也常常用,好比实现节流的时候,也要加一个标记变量。

管道

可是信号量不能传递具体的数据啊,传递具体数据还得用别的方式。好比咱们能够经过读写文件的方式来通讯,这就是管道,若是是在内存中的文件,叫作匿名管道,没有文件名,若是是真实的硬盘的文件,是有文件名的,叫作命名管道。

文件须要先打开,而后再读和写,以后再关闭,这也是管道的特色。管道是基于文件的思想封装的,之因此叫管道,是由于只能一个进程读、一个进程写,是单向的(半双工)。并且还须要目标进程同步的消费数据,否则就会阻塞住。

这种管道的方式实现起来很简单,就是一个文件读写,可是只能用在两个进程之间通讯,只能同步的通讯。其实管道的同步通讯也挺常见的,就是 stream 的 pipe 方法。

消息队列

管道实现简单,可是同步的通讯比较受限制,那若是想作成异步通讯呢?加个队列作缓冲(buffer)不就好了,这就是消息队列

消息队列也是两个进程之间的通讯,可是不是基于文件那一套思路,虽然也是单向的,可是有了必定的异步性,能够放不少消息,以后一次性消费。

共享内存

管道、消息队列都是两个进程之间的,若是多个进程之间呢?

咱们能够经过申请一段多进程均可以操做的内存,叫作共享内存,用这种方式来通讯。各进程均可以向该内存读写数据,效率比较高。

共享内存虽然效率高、也能用于多个进程的通讯,但也不全是好处,由于多个进程均可以读写,那么就很容易乱,要本身控制顺序,好比经过进程的信号量(标记变量)来控制。

共享内存适用于多个进程之间的通讯,不须要经过中间介质,因此效率更高,可是使用起来也更复杂。

上面说的这些几乎就是本地进程通讯的所有方式了,为何要加个本地呢?

ipc、rpc、lpc

进程通讯就是 ipc(Inter-Process Communication),两个进程多是一台计算机的,也可能网络上的不一样计算机的进程,因此进程通讯方式分为两种:

本地过程调用 LPC(local procedure call)、远程过程调用 RPC(remote procedure call)。

本地过程调用就是咱们上面说的信号量、管道、消息队列、共享内存的通讯方式,可是若是是网络上的,那就要经过网络协议来通讯了,这个其实咱们用的比较多,好比 http、websocket。

因此,当有人提到 ipc 时就是在说进程通讯,能够分为本地的和远程的两种来讨论。

远程的都是基于网络协议封装的,而本地的都是基于信号量、管道、消息队列、共享内存封装出来的,好比咱们接下来要探讨的 electron 和 nodejs。

electron 进程通讯

electron 会先启动主进程,而后经过 BrowserWindow 建立渲染进程,加载 html 页面实现渲染。这两个进程之间的通讯是经过 electron 提供的 ipc 的 api。

ipcMain、ipcRenderer

主进程里面经过 ipcMain 的 on 方法监听事件

import { ipcMain } from 'electron';

ipcMain.on('异步事件', (event, arg) => {
  event.sender.send('异步事件返回', 'yyy');
})
复制代码

渲染进程里面经过 ipcRenderer 的 on 方法监听事件,经过 send 发送消息

import { ipcRenderer } from 'electron';

ipcRender.on('异步事件返回', function (event, arg) {
  const message = `异步消息: ${arg}`
})

ipcRenderer.send('异步事件', 'xxx')
复制代码

api 使用比较简单,这是通过 c++ 层的封装,而后暴露给 js 的事件形式的 api。

咱们能够想一下它是基于哪一种机制实现的呢?

很明显有必定的异步性,并且是父子进程之间的通讯,因此是消息队列的方式实现的。

remote

除了事件形式的 api 外,electron 还提供了远程方法调用 rmi (remote method invoke)形式的 api。

其实就是对消息的进一步封装,也就是根据传递的消息,调用不一样的方法,形式上就像调用本进程的方法同样,但实际上是发消息到另外一个进程来作的,和 ipcMain、ipcRenderer 的形式本质上同样。

好比在渲染进程里面,经过 remote 来直接调用主进程才有的 BrowserWindow 的 api。

const { BrowserWindow } = require('electron').remote;

let win = new BrowserWindow({ width: 800, height: 600 });
win.loadURL('https://github.com');
复制代码

小结一下,electron 的父子进程通讯方式是基于消息队列封装的,封装形式有两种,一种是事件的方式,经过 ipcMain、ipcRenderer 的 api 使用,另外一种则是进一步封装成了不一样方法的调用(rmi),底层也是基于消息,执行远程方法可是看上去像执行本地方法同样。

nodejs

nodejs 提供了建立进程的 api,有两个模块: child_process 和 cluster。很明显,一个是用于父子进程的建立和通讯,一个是用于多个进程。

child_process

child_process 提供了 spawn、exec、execFile、fork 的 api,分别用于不一样的进程的建立:

spawn、exec

若是想经过 shell 执行命令,那就用 spawn 或者 exec。由于通常执行命令是须要返回值的,这俩 api 在返回值的方式上有所不一样。

spawn 返回的是 stream,经过 data 事件来取,exec 进一步分装成了 buffer,使用起来简单一些,可是可能会超过 maxBuffer。

const { spawn } = require('child_process'); 

var app = spawn('node','main.js' {env:{}});

app.stderr.on('data',function(data) {
  console.log('Error:',data);
});

app.stdout.on('data',function(data) {
  console.log(data);
});
复制代码

其实 exec 是基于 spwan 封装出来的,简单场景能够用,有的时候要设置下 maxBuffer。

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

exec('find . -type f', { maxBuffer: 1024*1024 }(err, stdout, stderr) => { 
    if (err) { 
        console.error(`exec error: ${err}`); return; 
    }   
    console.log(stdout); 
});
复制代码

execFile

除了执行命令外,若是要执行可执行文件就用 execFile 的 api:

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

const child = execFile('node', ['--version'], (error, stdout, stderr) => { 
    if (error) { throw error; } 
    console.log(stdout); 
});
复制代码

fork

还有若是是想执行 js ,那就用 fork:

const { fork } = require('child_process');	

const xxxProcess = fork('./xxx.js');	
xxxProcess.send('111111');	
xxxProcess.on('message', sum => {	
    res.end('22222');	
});
复制代码

小结

简单小结一下 child_process 的 4 个 api:

若是想执行 shell 命令,用 spawn 和 exec,spawn 返回一个 stream,而 exec 进一步封装成了 buffer。除了 exec 有的时候须要设置下 maxBuffer,其余没区别。

若是想执行可执行文件,用 execFile。

若是想执行 js 文件,用 fork。

child_process 的进程通讯

说完了 api 咱们来讲下 child_process 建立的子进程怎么和父进程通讯,也就是怎么作 ipc。

pipe

首先,支持了 pipe,很明显是经过管道的机制封装出来的,能同步的传输流的数据。

const { spawn } = require('child_process'); 

const find = spawn('cat', ['./aaa.js']);
const wc = spawn('wc', ['-l']);  find.stdout.pipe(wc.stdin);
复制代码

好比上面经过管道把一个进程的输出流传输到了另外一个进程的输入流,和下面的 shell 命令效果同样:

cat ./aaa.js | wc -l
复制代码

message

spawn 支持 stdio 参数,能够设置和父进程的 stdin、stdout、stderr 的关系,好比指定 pipe 或者 null。还有第四个参数,能够设置 ipc,这时候就是经过事件的方式传递消息了,很明显,是基于消息队列实现的。

const { spawn } = require('child_process');

const child = spawn('node', ['./child.js'], {
    stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 
}); 
child.on('message', (m) => { 
    console.log(m); 
}); 
child.send('xxxx');
复制代码

而 fork 的 api 建立的子进程自带了 ipc 的传递消息机制,能够直接用。

const { fork } = require('child_process');	

const xxxProcess = fork('./xxx.js');	
xxxProcess.send('111111');	
xxxProcess.on('message', sum => {	
    res.end('22222');	
});
复制代码

cluster

cluster 再也不是父子进程了,而是更多进程,也提供了 fork 的 api。

好比 http server 会根据 cpu 数启动多个进程来处理请求。

import cluster from 'cluster';
import http from 'http';
import { cpus } from 'os';
import process from 'process';

const numCPUs = cpus().length;

if (cluster.isPrimary) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
} else {
  const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');
  })
  
  server.listen(8000);
  
  process.on('message', (msg) => {
    if (msg === 'shutdown') {
       server.close();
    }
  });
}
复制代码

它一样支持了事件形式的 api,用于多个进程之间的消息传递,由于多个进程其实也只是多个父子进程的通讯,子进程之间不能直接通讯,因此仍是基于消息队列实现的。

共享内存

子进程之间通讯还得经过父进程中转一次,要屡次读写消息队列,效率过低了,就不能直接共享内存么?

如今 nodejs 仍是不支持的,能够经过第三方的包 shm-typed-array 来实现,感兴趣能够看一下。

www.npmjs.com/package/shm…

总结

进程包括代码、数据和 PCB,是程序的一次执行的过程,PCB 记录着各类执行过程当中的信息,好比分配的资源、执行到的地址、用于通讯的数据结构等。

进程之间须要通讯,能够经过信号量、管道、消息队列、共享内存的方式。

  • 信号量就是一个简单的数字的标记,不能传递具体数据。

  • 管道是基于文件的思想,一个进程写另外一个进程读,是同步的,适用于两个进程。

  • 消息队列有必定的 buffer,能够异步处理消息,适用于两个进程。

  • 共享内存是多个进程直接操做同一段内存,适用于多个进程,可是须要控制访问顺序。

这四种是本地进程的通讯方式,而网络进程则基于网络协议的方式也能够作进程通讯。

进程通讯叫作 ipc,本地的叫作 lpc,远程的叫 rpc。

其中,若是把消息再封装一层成具体的方法调用,叫作 rmi,效果就像在本进程执行执行另外一个进程的方法同样。

electron 和 nodejs 都是基于上面的操做系统机制的封装:

  • elctron 支持 ipcMain 和 ipcRenderer 的消息传递的方式,还支持了 remote 的 rmi 的方式。

  • nodejs 有 child_process 和 cluster 两个模块和进程有关,child_process 是父子进程之间,cluster 是多个进程:

    • child_process 提供了用于执行 shell 命令的 spawn、exec,用于执行可执行文件的 execFile,用于执行 js 的 fork。提供了 pipe 和 message 两种 ipc 方式。

    • cluster 也提供了 fork,提供了 message 的方式的通讯。

固然,无论封装形式是什么,都离不开操做系统提供的信号量、管道、消息队列、共享内存这四种机制。

ipc 是开发中频繁遇到的需求,但愿这篇文章可以帮你们梳理清楚从操做系统层到不一样语言和运行时的封装层次的脉络。

相关文章
相关标签/搜索