爲何前端要了解進程通訊:javascript
前端領域已經不是單純寫在瀏覽器裏跑的頁面就能夠了,還要會 electron、nodejs 等,而這倆技術都須要掌握進程通訊。html
nodejs 是 js 的一個運行時,和瀏覽器不一樣,它擴展了不少封裝操做系統能力的 api,其中就包括進程、線程相關 api,而學習進程 api 就要學習進程之間的通訊機制。前端
electron 是基於 chromium 和 nodejs 的桌面端開發方案,它的架構是一個主進程,多個渲染進程,這兩種進程之間也須要通訊,要學習 electron 的進程通訊機制。java
這篇文章咱們就來深刻了解一下進程通訊。node
本文會講解如下知識點:c++
咱們寫完的代碼要在操做系統之上跑,操做系統爲了更好的利用硬件資源,支持了多個程序的併發和硬件資源的分配,分配的單位就是進程,這個進程就是程序的執行過程。好比記錄程序執行到哪一步了,申請了哪些硬件資源、佔用了什麼端口等。git
進程包括要執行的代碼、代碼操做的數據,以及進程控制塊 PCB(Processing Control Block),由於程序就是代碼在數據集上的執行過程,而執行過程的狀態和申請的資源須要記錄在一個數據結構(PCB)裏。因此進程由代碼、數據、PCB 組成。github
pcb 中記錄着 pid、執行到的代碼地址、進程的狀態(阻塞、運行、就緒等)以及用於通訊的信號量、管道、消息隊列等數據結構。web
進程從建立到代碼不斷的執行,到申請硬件資源(內存、硬盤文件、網絡等),中間還可能會阻塞,最終執行完會銷燬進程。這是一個進程的生命週期。shell
進程對申請來的資源是獨佔式的,每一個進程都只能訪問本身的資源,那進程之間怎麼通訊呢?
不一樣進程之間由於可用的內存不一樣,因此要經過一箇中間介質通訊。
若是是簡單的標記,經過一個數字來表示,放在 PCB 的一個屬性裏,這叫作信號量
,好比鎖的實現就能夠經過信號量。
這種信號量的思想咱們寫前端代碼也常常用,好比實現節流的時候,也要加一個標記變量。
可是信號量不能傳遞具體的數據啊,傳遞具體數據還得用別的方式。好比咱們能夠經過讀寫文件的方式來通訊,這就是管道
,若是是在內存中的文件,叫作匿名管道,沒有文件名,若是是真實的硬盤的文件,是有文件名的,叫作命名管道。
文件須要先打開,而後再讀和寫,以後再關閉,這也是管道的特色。管道是基於文件的思想封裝的,之因此叫管道,是由於只能一個進程讀、一個進程寫,是單向的(半雙工)。並且還須要目標進程同步的消費數據,否則就會阻塞住。
這種管道的方式實現起來很簡單,就是一個文件讀寫,可是隻能用在兩個進程之間通訊,只能同步的通訊。其實管道的同步通訊也挺常見的,就是 stream 的 pipe 方法。
管道實現簡單,可是同步的通訊比較受限制,那若是想作成異步通訊呢?加個隊列作緩衝(buffer)不就好了,這就是消息隊列
。
消息隊列也是兩個進程之間的通訊,可是不是基於文件那一套思路,雖然也是單向的,可是有了必定的異步性,能夠放不少消息,以後一次性消費。
管道、消息隊列都是兩個進程之間的,若是多個進程之間呢?
咱們能夠經過申請一段多進程均可以操做的內存,叫作共享內存
,用這種方式來通訊。各進程均可以向該內存讀寫數據,效率比較高。
共享內存雖然效率高、也能用於多個進程的通訊,但也不全是好處,由於多個進程均可以讀寫,那麼就很容易亂,要本身控制順序,好比經過進程的信號量(標記變量)來控制。
共享內存適用於多個進程之間的通訊,不須要經過中間介質,因此效率更高,可是使用起來也更復雜。
上面說的這些幾乎就是本地進程通訊的所有方式了,爲何要加個本地呢?
進程通訊就是 ipc(Inter-Process Communication),兩個進程多是一臺計算機的,也可能網絡上的不一樣計算機的進程,因此進程通訊方式分爲兩種:
本地過程調用 LPC(local procedure call)、遠程過程調用 RPC(remote procedure call)。
本地過程調用就是咱們上面說的信號量、管道、消息隊列、共享內存的通訊方式,可是若是是網絡上的,那就要經過網絡協議來通訊了,這個其實咱們用的比較多,好比 http、websocket。
因此,當有人提到 ipc 時就是在說進程通訊,能夠分爲本地的和遠程的兩種來討論。
遠程的都是基於網絡協議封裝的,而本地的都是基於信號量、管道、消息隊列、共享內存封裝出來的,好比咱們接下來要探討的 electron 和 nodejs。
electron 會先啓動主進程,而後經過 BrowserWindow 建立渲染進程,加載 html 頁面實現渲染。這兩個進程之間的通訊是經過 electron 提供的 ipc 的 api。
主進程裏面經過 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。
咱們能夠想一下它是基於哪一種機制實現的呢?
很明顯有必定的異步性,並且是父子進程之間的通訊,因此是消息隊列的方式實現的。
除了事件形式的 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 提供了建立進程的 api,有兩個模塊: child_process 和 cluster。很明顯,一個是用於父子進程的建立和通訊,一個是用於多個進程。
child_process 提供了 spawn、exec、execFile、fork 的 api,分別用於不一樣的進程的建立:
若是想經過 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 的 api:
const { execFile } = require('child_process');
const child = execFile('node', ['--version'], (error, stdout, stderr) => {
if (error) { throw error; }
console.log(stdout);
});
複製代碼
還有若是是想執行 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。
說完了 api 咱們來講下 child_process 建立的子進程怎麼和父進程通訊,也就是怎麼作 ipc。
首先,支持了 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
複製代碼
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 再也不是父子進程了,而是更多進程,也提供了 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 來實現,感興趣能夠看一下。
進程包括代碼、數據和 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 是開發中頻繁遇到的需求,但願這篇文章可以幫你們梳理清楚從操做系統層到不一樣語言和運行時的封裝層次的脈絡。