前端需要了解的进程与进程通讯知识

本文目录:

  • 1.为什么前端要了解进程通讯
  • 2.进程是什么
  • 3.本地进程通信的四种方式
  • 4.ipc、lpc、rpc 都是什么
  • 5.electron 如何做进程通信
  • 6.nodejs 的 child_process 和 cluster 如何做进程通信

1.为什么前端要了解进程通讯

前端领域已经不是单纯写在浏览器里跑的页面就可以了,还要会 electron、nodejs 等,而这俩技术都需要掌握进程通信。
nodejs 是 js 的一个运行时,和浏览器不同,它扩展了很多封装操作系统能力的 api,其中就包括进程、线程相关 api,而学习进程 api 就要学习进程之间的通信机制。
electron 是基于 chromium 和 nodejs 的桌面端开发方案,它的架构是一个主进程,多个渲染进程,这两种进程之间也需要通信,要学习 electron 的进程通信机制。

2.进程是什么

我们写完的代码要在操作系统之上跑,操作系统为了更好的利用硬件资源,支持了多个程序的并发和硬件资源的分配,分配的单位就是进程,这个进程就是程序的执行过程。比如记录程序执行到哪一步了,申请了哪些硬件资源、占用了什么端口等。
进程包括要执行的代码、代码操作的数据,以及进程控制块 PCB(Processing Control Block),因为程序就是代码在数据集上的执行过程,而执行过程的状态和申请的资源需要记录在一个数据结构(PCB)里。所以进程由代码、数据、PCB 组成。
pcb 中记录着 pid、执行到的代码地址、进程的状态(阻塞、运行、就绪等)以及用于通信的信号量、管道、消息队列等数据结构。
进程从创建到代码不断的执行,到申请硬件资源(内存、硬盘文件、网络等),中间还可能会阻塞,最终执行完会销毁进程。这是一个进程的生命周期。
进程对申请来的资源是独占式的,每个进程都只能访问自己的资源,那进程之间怎么通信呢?

3.本地进程通信的四种方式

不同进程之间因为可用的内存不同,所以要通过一个中间介质通信。

1.信号量

如果是简单的标记,通过一个数字来表示,放在 PCB 的一个属性里,这叫做信号量,比如锁的实现就可以通过信号量。

2.管道

但是信号量不能传递具体的数据啊,传递具体数据还得用别的方式。比如我们可以通过读写文件的方式来通信,这就是管道,如果是在内存中的文件,叫做匿名管道,没有文件名,如果是真实的硬盘的文件,是有文件名的,叫做命名管道。
文件需要先打开,然后再读和写,之后再关闭,这也是管道的特点。管道是基于文件的思想封装的,之所以叫管道,是因为只能一个进程读、一个进程写,是单向的(半双工)。而且还需要目标进程同步的消费数据,不然就会阻塞住。
这种管道的方式实现起来很简单,就是一个文件读写,但是只能用在两个进程之间通信,只能同步的通信。其实管道的同步通信也挺常见的,就是 stream 的 pipe 方法。

3.消息队列

管道实现简单,但是同步的通信比较受限制,那如果想做成异步通信呢?加个队列做缓冲(buffer)不就行了,这就是消息队列。
消息队列也是两个进程之间的通信,但是不是基于文件那一套思路,虽然也是单向的,但是有了一定的异步性,可以放很多消息,之后一次性消费。

4.共享内存

管道、消息队列都是两个进程之间的,如果多个进程之间呢?
我们可以通过申请一段多进程都可以操作的内存,叫做共享内存,用这种方式来通信。各进程都可以向该内存读写数据,效率比较高。
共享内存虽然效率高、也能用于多个进程的通信,但也不全是好处,因为多个进程都可以读写,那么就很容易乱,要自己控制顺序,比如通过进程的信号量(标记变量)来控制。
共享内存适用于多个进程之间的通信,不需要通过中间介质,所以效率更高,但是使用起来也更复杂。
上面说的这些几乎就是本地进程通信的全部方式了,为什么要加个本地呢?

4.ipc、lpc、rpc 都是什么

进程通信就是 ipc(Inter-Process Communication),两个进程可能是一台计算机的,也可能网络上的不同计算机的进程,所以进程通信方式分为两种:
本地过程调用 LPC(local procedure call)、远程过程调用 RPC(remote procedure call)。
本地过程调用就是我们上面说的信号量、管道、消息队列、共享内存的通信方式,但是如果是网络上的,那就要通过网络协议来通信了,这个其实我们用的比较多,比如 http、websocket。
所以,当有人提到 ipc 时就是在说进程通信,可以分为本地的和远程的两种来讨论。
远程的都是基于网络协议封装的,而本地的都是基于信号量、管道、消息队列、共享内存封装出来的,比如我们接下来要探讨的 electron 和 nodejs。

5.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),底层也是基于消息,执行远程方法但是看上去像执行本地方法一样。

6.nodejs 的 child_process 和 cluster 如何做进程通信

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 来实现,感兴趣可以看一下。
https://www.npmjs.com/package/shm-typed-array

总结

进程包括代码、数据和 PCB,是程序的一次执行的过程,PCB 记录着各种执行过程中的信息,比如分配的资源、执行到的地址、用于通信的数据结构等。
进程之间需要通信,可以通过信号量、管道、消息队列、共享内存的方式。

  • 信号量就是一个简单的数字的标记,不能传递具体数据。
  • 管道是基于文件的思想,一个进程写另一个进程读,是同步的,适用于两个进程。
  • 消息队列有一定的 buffer,可以异步处理消息,适用于两个进程。
  • 共享内存是多个进程直接操作同一段内存,适用于多个进程,但是需要控制访问顺序。

这四种是本地进程的通信方式,而网络进程则基于网络协议的方式也可以做进程通信。

进程通信叫做 ipc,本地的叫做 lpc,远程的叫 rpc。
其中,如果把消息再封装一层成具体的方法调用,叫做 rmi,效果就像在本进程执行执行另一个进程的方法一样。
electron 和 nodejs 都是基于上面的操作系统机制的封装:

  • electron 支持 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 的方式的通信。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容