node進程間通訊
作為一名合格的程式猿/媛,對於進程、執行緒還是有必要了解一點的,本文將從下面幾個方向進行梳理,盡量做到知其然並知其所以然:
- 進程和執行緒的概念和關係
- 進程演進
- 進程間通訊
- 理解底層基礎,助力上層應用
- 進程保護
進程和執行緒的概念和關係
用戶下達運行程式的命令後,就會產生進程。同一程式可產生多個進程(一對多關係),以允許同時有多位用戶運行同一程式,卻不會相衝突。
進程需要一些資源才能完成工作,如CPU使用時間、存儲器、文件以及I/O設備,且為依序逐一進行,也就是每個CPU核心任何時間內僅能運行一項進程。
進程與執行緒的區別:進程是電腦管理運行程式的一種方式,一個進程下可包含一個或者多個執行緒。執行緒可以理解為子進程。
摘自wiki百科
也就是說,進程是我們運行的程式程式碼和佔用的資源總和,執行緒是進程的最小執行單位,當然也支援並發。可以說是把問題細化,分成一個個更小的問題,進而得以解決。
並且進程內的執行緒是共享進程資源的,處於同一地址空間,所以切換和通訊相對成本小,而進程可以理解為沒有公共的包裹容器。
但是如果進程間需要通訊的話,也需要一個公共環境或者一個媒介,這個就是作業系統。
進程演進
我們的電腦有單核的、多核的,也有多種的組合方式:
- 單進程
因為是一個進程,所以某一時刻只能處理一個事務,後續需要等待,體驗不好
- 多進程
為了解決上面的問題,但是如果有很多請求的話,會產生很多進程,開銷本身就是一個不小的問題,而進程佔據獨立的記憶體,這麼多響應是的進程難免會有重複的狀態和數據,會造成資源浪費。
- 多進程多執行緒
由之前的進程處理事務,改成使用執行緒處理事務,解決了開銷大,資源浪費的問題,還可以使用執行緒池,預先創建就緒執行緒,減少創建和銷毀執行緒的開銷。
但是一個cpu某一時刻只能處理一個事務。像時間分片來調度執行緒的話,會導致執行緒切換頻繁,是非常耗時的。
- 單進程單執行緒
類似也就是v8,基於事件驅動,有效的避免了記憶體開銷和上下文切換,只需要執行緒間通訊,即可在適當的時刻進行事務結果等的回饋。
但是遇到計算量很大的事務,會阻塞後續任務的執行。像這樣:
- 單進程單執行緒(多進程架構)
node提供了cluster和child_process兩個模組進行進程的創建,也就是我們常說的主(Master)從(Worker)模式。Master負責任務調度和管理Worker進程,Worker進行事務處理。
進程間通訊
node本身提供了cluster和child_process模組創建子進程,本質上cluster.fork()是child_process.fork()的上層實現,cluster帶來的好處是可以監聽共享埠,否則建議使用child_process。
child_process
child_process提供了非同步和同步的操作方法,具體可查看文檔。
常見的非同步方法有:
除了fork出來的進程會長期駐存外,其他方式會在子進程任務完成後以流的方式返回並銷毀進程。
非同步方法會返回ChildProcess的實例,ChildProcess不能直接創建,只能返回。
來看幾張圖吧:
舉個例子
有一個很長很長的循環,如果不開啟子進程,會等循環之後才能執行之後的邏輯。
我們可以將耗時的循環放到子進程中,主進程會接受子進程的返回,不影響後續事物的處理。
// 主進程
const execFile = require('child_process').execFile;
execFile('./child.js', [], (err, stdout, stderr) => {
if (err) {
console.log(err);
return;
}
console.log(`stdout: ${stdout}`);
});
console.log('用戶事務處理');
// 子進程
#!/usr/bin/env node
for (let i = 0; i < 10000; i++) {
process.stdout.write(`${i}`);
}
而對於fork,它是專門用來生產子進程的,也可以說是主進程的拷貝,返回的ChildProcess中會內置額外的通訊通道,也就是IPC通道,允許消息在父子進程間傳遞,例如通過文件描述符,不過由於創建的是匿名通道,所以只有主進程可以與之通訊,其他進程無法進行通訊。但相對的還有命名通道,詳見下一節。
看一個簡單的例子:
//parent.js
const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);
n.on('message', (m) => {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
//sub.js
process.on('message', (m) => {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
父進程通過fork返回的ChildProcess進行通訊的監聽和發送,子進程通過全局變數process進行監聽和發送。
cluster
cluster本質上也是通過child_process.fork創建子進程,他還能幫我們合理的管理進程。
const cluster = require('cluster');
// 判斷是否為主進程
if (cluster.isMaster) {
const cpuNum = require('os').cpus().length;
for (let i = 0; i < cpuNum; ++i) {
cluster.fork();
}
cluster.on('online', (worker) => {
console.log('Create worker-' + worker.process.pid);
});
cluster.on('exit', (worker, code, signal) => {
console.log(
'[Master] worker ' +
worker.process.pid +
' died with code:' +
code +
', and' +
signal
);
cluster.fork(); // 重啟子進程
});
} else {
const net = require('net');
net.createServer()
.on('connection', (socket) => {
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid);
}, 10);
})
.listen(8989);
}
細心地你可能發現多個子進程監聽了同一個埠,這樣不會EADDRIUNS嗎?
其實不然,真正監聽埠的是主進程,當前端請求到達時,會將句柄發送給某個子進程。
理解底層基礎,助力上層應用
進程間通訊(IPC)大概有這幾種:
- 匿名管道
- 命名管道
- 訊號量
- 消息隊列
- 訊號
- 共享記憶體
- 套接字
從技術上劃分又可以劃分成以下四種:
- 消息傳遞(管道,FIFO,消息隊列)
- 同步(互斥量,條件變數,讀寫鎖等)
- 共享記憶體(匿名的,命名的)
- 遠程過程調用
文件描述符是什麼?
在linux中一切皆文件,linux會給每個文件分配一個id,這個id就是文件描述符,指針也是文件描述符的一種。這個很好理解,不過我們可以再往深了說,一個進程啟動後,會在內核空間(虛擬空間的一部分)創建一個PCB控制塊,PCB內部有一個文件描述符表,記錄著當前進程所有可用的文件描述符(即當前進程所有打開的文件)。系統出了維護文件描述符表外,還需要維護打開文件表(Open file table)和i-node表(i-node table)。
文件打開表(Open file table)包含文件偏移量,狀態標誌,i-node表指針等資訊
i-node表(i-node table)包括文件類型,文件大小,時間戳,文件鎖等資訊
文件描述符不是一對一的,它可以:
- 同一進程的不同文件描述符指向同一文件
- 不同進程可以擁有相同的文件描述符(比如fork出的子進程擁有和父進程一樣的文件描述符,或者不同進程打開同一文件)
- 不同進程的同一文件描述符也可以指向不同的文件
- 不同進程的不同文件描述符也可以指向同一個文件
上面提及了很多可以實現進程間通訊的方式,那node進程間通訊是以什麼為基礎的呢?
nodeIPC通過管道技術 加 事件循環方式進行通訊,管道技術在windows下由命名管道實現,在*nix系統則由Unix Domain socket實現,提供給我們的是簡單的message事件和send方法。
那管道是什麼呢?
管道實際上是在內核中開闢一塊緩衝區,它有一個讀端一個寫端,並傳給用戶程式兩個文件描述符,一個指向讀端,一個指向寫埠,然後該快取區存儲不同進程間寫入的內容,並供不同進程讀取內容,進而達到通訊的目的。
管道又分為匿名管道和命名管道,匿名管道常見於一個進程fork出子進程,只能親緣進程通訊,而命名管道可以讓非親緣進程進行通訊。
其實本質上來說進程間通訊是利用內核管理一塊記憶體,不同進程可以讀寫這塊內容,進而可以互相通訊,當然,說起來簡單,做起來難。有興趣的朋友可以自行研究。
進程保護
可以用cluster建立主從進程架構,主進程調度管理和分發任務給子進程,並在子進程掛掉或斷開連接後重啟。
pm2是對cluster的一種封裝,提供了:
- 內奸負載均衡
- 後台運行
- 停機重載
- 具有Ubuntu、CentOS的啟動腳本
- 停止不穩定的進程
- 控制台檢測
- 有好的可視化介面
具體原理和細節以後有空再做分析。
文中若有錯誤的地方,歡迎指出,我會及時更新。希望讀者借鑒的閱讀。

