做爲一名合格的程序猿/媛,對於進程、線程仍是有必要了解一點的,本文將從下面幾個方向進行梳理,儘可能作到知其然並知其因此然:javascript
用戶下達運行程序的命令後,就會產生進程。同一程序可產生多個進程(一對多關係),以容許同時有多位用戶運行同一程序,卻不會相沖突。html
進程須要一些資源才能完成工做,如CPU使用時間、存儲器、文件以及I/O設備,且爲依序逐一進行,也就是每一個CPU核心任什麼時候間內僅能運行一項進程。前端
進程與線程的區別:進程是計算機管理運行程序的一種方式,一個進程下可包含一個或者多個線程。線程能夠理解爲子進程。java
摘自wiki百科node
也就是說,進程是咱們運行的程序代碼和佔用的資源總和,線程是進程的最小執行單位,固然也支持併發。能夠說是把問題細化,分紅一個個更小的問題,進而得以解決。linux
而且進程內的線程是共享進程資源的,處於同一地址空間,因此切換和通訊相對成本小,而進程能夠理解爲沒有公共的包裹容器
。git
可是若是進程間須要通訊的話,也須要一個公共環境或者一個媒介,這個就是操做系統。github
咱們的計算機有單核的、多核的,也有多種的組合方式:chrome
由於是一個進程,因此某一時刻只能處理一個事務,後續須要等待,體驗很差windows
爲了解決上面的問題,可是若是有不少請求的話,會產生不少進程,開銷自己就是一個不小的問題,而進程佔據獨立的內存,這麼多響應是的進程不免會有重複的狀態和數據,會形成資源浪費。
由以前的進程處理事務,改爲使用線程處理事務,解決了開銷大,資源浪費的問題,還可使用線程池,預先建立就緒線程,減小建立和銷燬線程的開銷。
可是一個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提供了異步和同步的操做方法,具體可查看文檔。
常見的異步方法有:
除了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本質上也是經過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)大概有這幾種:
從技術上劃分又能夠劃分紅如下四種:
文件描述符是什麼?
在linux中一切皆文件,linux會給每一個文件分配一個id,這個id就是文件描述符,指針也是文件描述符的一種。這個很好理解,不過咱們能夠再往深了說,一個進程啓動後,會在內核空間(虛擬空間的一部分)建立一個PCB控制塊,PCB內部有一個文件描述符表,記錄着當前進程全部可用的文件描述符(即當前進程全部打開的文件)。系統出了維護文件描述符表外,還須要維護打開文件表(Open file table)和i-node表(i-node table)。
文件打開表(Open file table)包含文件偏移量,狀態標誌,i-node表指針等信息
i-node表(i-node table)包括文件類型,文件大小,時間戳,文件鎖等信息
文件描述符不是一對一的,它能夠:
上面說起了不少能夠實現進程間通訊的方式,那node進程間通訊是以什麼爲基礎的呢?
nodeIPC經過管道技術 加 事件循環方式進行通訊,管道技術在windows下由命名管道實現,在*nix系統則由Unix Domain socket實現,提供給咱們的是簡單的message事件和send方法。
那管道是什麼呢?
管道其實是在內核中開闢一塊緩衝區,它有一個讀端一個寫端,並傳給用戶程序兩個文件描述符,一個指向讀端,一個指向寫端口,而後該緩存區存儲不一樣進程間寫入的內容,並供不一樣進程讀取內容,進而達到通訊的目的。
管道又分爲匿名管道和命名管道,匿名管道常見於一個進程fork出子進程,只能親緣進程通訊,而命名管道可讓非親緣進程進行通訊。
其實本質上來講進程間通訊是利用內核管理一塊內存,不一樣進程能夠讀寫這塊內容,進而能夠互相通訊,固然,提及來簡單,作起來難。有興趣的朋友能夠自行研究。
能夠用cluster創建主從進程架構,主進程調度管理和分發任務給子進程,並在子進程掛掉或斷開鏈接後重啓。
pm2是對cluster的一種封裝,提供了:
具體原理和細節之後有空再作分析。
文中如有錯誤的地方,歡迎指出,我會及時更新。但願讀者借鑑的閱讀。