作為一名合格的程序猿/媛,對於進程、線程還是有必要了解一點的,本文將從下面幾個方向進行梳理,盡量做到知其然並知其所以然:
- 進程和線程的概念和關系
- 進程演進
- 進程間通信
- 理解底層基礎,助力上層應用
- 進程保護
進程和線程的概念和關系
用戶下達運行程序的命令后,就會產生進程。同一程序可產生多個進程(一對多關系),以允許同時有多位用戶運行同一程序,卻不會相沖突。
進程需要一些資源才能完成工作,如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的啟動腳本
- 停止不穩定的進程
- 控制台檢測
- 有好的可視化界面
具體原理和細節以后有空再做分析。
文中若有錯誤的地方,歡迎指出,我會及時更新。希望讀者借鑒的閱讀。