您好,登錄后才能下訂單哦!
這篇文章主要介紹“NodeJS中的進程管理怎么實現”,在日常操作中,相信很多人在NodeJS中的進程管理怎么實現問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”NodeJS中的進程管理怎么實現”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
熟悉 js 的朋友都知道,js 是單線程
的,在 Node 中,采用的是 多進程單線程 的模型。由于javascript單線程的限制,在多核服務器上,我們往往需要啟動多個進程才能最大化服務器性能。
Node.js 進程集群可用于運行多個 Node.js 實例,這些實例可以在其應用程序線程之間分配工作負載。 當不需要進程隔離時,請改用 worker_threads
模塊,它允許在單個 Node.js 實例中運行多個應用程序線程。
進程總數,其中一個主進程,cpu 個數 x cpu 核數 個 子進程
無論 child_process 還是 cluster,都不是多線程模型,而是多進程模型
應對單線程問題,通常使用多進程的方式來模擬多線程
Node 在 V0.8 版本之后引入了 cluster模塊,通過一個主進程 (master) 管理多個子進程 (worker) 的方式實現集群
。
集群模塊可以輕松創建共享服務器端口的子進程。
cluster 底層是 child_process 模塊,除了可以發送普通消息,還可以發送底層對象
TCP
、UDP
等,cluster
模塊是child_process
模塊和net
模塊的組合應用。 cluster 啟動時,內部會啟動 TCP 服務器,將這個 TCP 服務器端 socket 的文件描述符發給工作進程。
在 cluster
模塊應用中,一個主進程只能管理一組工作進程
,其運作模式沒有 child_process
模塊那么靈活,但是更加穩定:
const cluster = require('cluster')復
.isMaster
標識主進程, Node<16
.isPrimary
標識主進程, Node>16
.isWorker
標識子進程
.worker
對當前工作進程對象的引用【子進程中】
.workers
存儲活動工作進程對象的哈希,以 id
字段為鍵。 這樣可以很容易地遍歷所有工作進程。 它僅在主進程中可用。cluster.wokers[id] === worker
【主進程中】
.settings
只讀, cluster配置項。在調用 .setupPrimary()或.fork()方法之后,此設置對象將包含設置,包括默認值。之前為空對象。此對象不應手動更改或設置。
cluster.settings
配置項詳情:- `execArgv` <string[]>傳給 Node.js 可執行文件的字符串參數列表。 **默認值:** `process.execArgv`。 - `exec` <string> 工作進程文件的文件路徑。 **默認值:** `process.argv[1]`。 - `args` <string[]> 傳給工作進程的字符串參數。 **默認值:**`process.argv.slice(2)`。 - `cwd` <string>工作進程的當前工作目錄。 **默認值:** `undefined` (從父進程繼承)。 - `serialization` <string>指定用于在進程之間發送消息的序列化類型。 可能的值為 `'json'` 和 `'advanced'`。 **默認值:** `false`。 - `silent` <boolean>是否將輸出發送到父進程的標準輸入輸出。 **默認值:** `false`。 - `stdio` <Array>配置衍生進程的標準輸入輸出。 由于集群模塊依賴 IPC 來運行,因此此配置必須包含 `'ipc'` 條目。 提供此選項時,它會覆蓋 `silent`。 - `uid` <number>設置進程的用戶標識。 - `gid` <number>設置進程的群組標識。 - `inspectPort` <number> | <Function> 設置工作進程的檢查器端口。 這可以是數字,也可以是不帶參數并返回數字的函數。 默認情況下,每個工作進程都有自己的端口,從主進程的 `process.debugPort` 開始遞增。 - `windowsHide` <boolean> 隱藏通常在 Windows 系統上創建的衍生進程控制臺窗口。 **默認值:** `false`。
.fork([env])
衍生新的工作進程【主進程中】
.setupPrimary([settings])
Node>16
.setupMaster([settings])
用于更改默認的 'fork' 行為,用后設置將出現在 cluster.settings
中。任何設置更改只會影響未來對 .fork()
的調用,而不會影響已經運行的工作進程。上述默認值僅適用于第一次調用。Node 小于 16【主進程中】
.disconnect([callback])
當所有工作進程斷開連接并關閉句柄時調用【主進程中】
為了讓集群更加穩定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 當集群主進程接收到來自任何工作進程的消息時觸發。
'exit'
事件, 當任何工作進程死亡時,則集群模塊將觸發 'exit'
事件。
cluster.on('exit', (worker, code, signal) => { console.log('worker %d died (%s). restarting...', worker.process.pid, signal || code); cluster.fork(); });
'listening'
事件,從工作進程調用 listen()
后,當服務器上觸發 'listening'
事件時,則主進程中的 cluster
也將觸發 'listening'
事件。
cluster.on('listening', (worker, address) => { console.log( `A worker is now connected to ${address.address}:${address.port}`); });
'fork'
事件,當新的工作進程被衍生時,則集群模塊將觸發 'fork'
事件。
cluster.on('fork', (worker) => { timeouts[worker.id] = setTimeout(errorMsg, 2000); });
'setup'
事件,每次調用 .setupPrimary()
時觸發。
disconnect
事件,在工作進程 IPC 通道斷開連接后觸發。 當工作進程正常退出、被殺死、或手動斷開連接時
cluster.on('disconnect', (worker) => { console.log(`The worker #${worker.id} has disconnected`); });
Worker
對象包含了工作進程的所有公共的信息和方法。 在主進程中,可以使用 cluster.workers
來獲取它。 在工作進程中,可以使用 cluster.worker
來獲取它。
.id
工作進程標識,每個新的工作進程都被賦予了自己唯一的 id,此 id 存儲在 id
。當工作進程存活時,這是在 cluster.workers
中索引它的鍵。
.process
所有工作進程都是使用 child_process.fork()
創建,此函數返回的對象存儲為 .process
。 在工作進程中,存儲了全局的 process
。
.send(message[, sendHandle[, options]][, callback])
向工作進程或主進程發送消息,可選擇使用句柄。在主進程中,這會向特定的工作進程發送消息。 它與 ChildProcess.send()
相同。在工作進程中,這會向主進程發送消息。 它與 process.send()
相同。
.destroy()
.kill([signal])
此函數會殺死工作進程。kill()
函數在不等待正常斷開連接的情況下殺死工作進程,它與 worker.process.kill()
具有相同的行為。為了向后兼容,此方法別名為 worker.destroy()
。
.disconnect([callback])
發送給工作進程,使其調用自身的 .disconnect()
將關閉所有服務器,等待那些服務器上的 'close'
事件,然后斷開 IPC 通道。
.isConnect()
如果工作進程通過其 IPC 通道連接到其主進程,則此函數返回 true
,否則返回 false
。 工作進程在創建后連接到其主進程。
.isDead()
如果工作進程已終止(由于退出或收到信號),則此函數返回 true
。 否則,它返回 false
。
為了讓集群更加穩定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 在工作進程中。
cluster.workers[id].on('message', messageHandler);
'exit'
事件, 當任何工作進程死亡時,則當前worker工作進程
對象將觸發 'exit'
事件。
if (cluster.isPrimary) { const worker = cluster.fork(); worker.on('exit', (code, signal) => { if (signal) { console.log(`worker was killed by signal: ${signal}`); } else if (code !== 0) { console.log(`worker exited with error code: ${code}`); } else { console.log('worker success!'); } }); }
'listening'
事件,從工作進程調用 listen()
,對當前工作進程進行監聽。
cluster.fork().on('listening', (address) => { // 工作進程正在監聽 });
disconnect
事件,在工作進程 IPC 通道斷開連接后觸發。 當工作進程正常退出、被殺死、或手動斷開連接時
cluster.fork().on('disconnect', () => { //限定于當前worker對象觸發 });
Node中主進程和子進程之間通過進程間通信 (IPC) 實現進程間的通信,進程間通過 .send()
(a.send表示向a發送)方法發送消息,監聽 message
事件收取信息,這是 cluster模塊
通過集成 EventEmitter
實現的。還是一個簡單的官網的進程間通信例子
子進程:process.on('message')
、process.send()
父進程:child.on('message')
、child.send()
# cluster.isMaster # cluster.fork() # cluster.workers # cluster.workers[id].on('message', messageHandler); # cluster.workers[id].send(); # process.on('message', messageHandler); # process.send(); const cluster = require('cluster'); const http = require('http'); # 主進程 if (cluster.isMaster) { // Keep track of http requests console.log(`Primary ${process.pid} is running`); let numReqs = 0; // Count requests function messageHandler(msg) { if (msg.cmd && msg.cmd === 'notifyRequest') { numReqs += 1; } } // Start workers and listen for messages containing notifyRequest // 開啟多進程(cpu核心數) // 衍生工作進程。 const numCPUs = require('os').cpus().length; for (let i = 0; i < numCPUs; i++) { console.log(i) cluster.fork(); } // cluster worker 主進程與子進程通信 for (const id in cluster.workers) { // ***監聽來自子進程的事件 cluster.workers[id].on('message', messageHandler); // ***向子進程發送 cluster.workers[id].send({ type: 'masterToWorker', from: 'master', data: { number: Math.floor(Math.random() * 50) } }); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { # 子進程 // 工作進程可以共享任何 TCP 連接 // 在本示例中,其是 HTTP 服務器 // Worker processes have a http server. http.Server((req, res) => { res.writeHead(200); res.end('hello world\n'); //****** !!!!Notify master about the request !!!!!!******* //****** 向process發送 process.send({ cmd: 'notifyRequest' }); //****** 監聽從process來的 process.on('message', function(message) { // xxxxxxx }) }).listen(8000); console.log(`Worker ${process.pid} started`); }
NodeJS 進程之間通信只有消息傳遞,不會真正的傳遞對象。
send()
方法在發送消息前,會將消息組裝成 handle 和 message,這個 message 會經過 JSON.stringify
序列化,也就是說,傳遞句柄的時候,不會將整個對象傳遞過去,在 IPC 通道傳輸的都是字符串,傳輸后通過 JSON.parse
還原成對象。
代碼里有 app.listen(port)
在進行 fork 時,為什么多個進程可以監聽同一個端口呢?
原因是主進程通過 send() 方法向多個子進程發送屬于該主進程的一個服務對象的句柄,所以對于每一個子進程而言,它們在還原句柄之后,得到的服務對象是一樣的,當網絡請求向服務端發起時,進程服務是搶占式的,所以監聽相同端口時不會引起異常。
看下端口被占用的情況:
# master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); for (let i=0; i<cpus.length; i++) { const worker = fork('worker.js'); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); }
# worker.js const http = require('http'); http.createServer((req, res) => { res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); }).listen(3000);
以上代碼示例,控制臺執行
node master.js
只有一個 worker 可以監聽到 3000 端口,其余將會拋出Error: listen EADDRINUSE :::3000
錯誤。
那么多進程模式下怎么實現多進程端口監聽呢?答案還是有的,通過句柄傳遞 Node.js v0.5.9 版本之后支持進程間可發送句柄
功能
/** * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback * message * sendHandle */ subprocess.send(message, sendHandle)
當父子進程之間建立 IPC 通道之后,通過子進程對象的 send 方法發送消息,第二個參數 sendHandle 就是句柄,可以是 TCP套接字、TCP服務器、UDP套接字等
,為了解決上面多進程端口占用問題,我們將主進程的 socket 傳遞到子進程。
# master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); const server = require('net').createServer(); server.listen(3000); process.title = 'node-master' for (let i=0; i<cpus.length; i++) { const worker = fork('worker.js'); # 句柄傳遞 worker.send('server', server); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); }
// worker.js let worker; process.title = 'node-worker' process.on('message', function (message, sendHandle) { if (message === 'server') { worker = sendHandle; worker.on('connection', function (socket) { console.log('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid) }); } });
驗證一番,控制臺執行 node master.js
了解 cluster
的話會知道,子進程是通過 cluster.fork()
創建的。在 linux 中,系統原生提供了 fork
方法,那么為什么 Node 選擇自己實現 cluster模塊
,而不是直接使用系統原生的方法?主要的原因是以下兩點:
fork的進程監聽同一端口會導致端口占用錯誤
fork的進程之間沒有負載均衡,容易導致驚群現象
在 cluster模塊
中,針對第一個問題,通過判斷當前進程是否為 master進程
,若是,則監聽端口,若不是則表示為 fork 的 worker進程
,不監聽端口。
針對第二個問題,cluster模塊
內置了負載均衡功能, master進程
負責監聽端口接收請求,然后通過調度算法(默認為 Round-Robin,可以通過環境變量 NODE_CLUSTER_SCHED_POLICY
修改調度算法)分配給對應的 worker進程
。
當代碼拋出了異常沒有被捕獲到時,進程將會退出,此時 Node.js 提供了 process.on('uncaughtException', handler)
接口來捕獲它,但是當一個 Worker 進程遇到未捕獲的異常時,它已經處于一個不確定狀態,此時我們應該讓這個進程優雅退出:
關閉異常 Worker 進程所有的 TCP Server(將已有的連接快速斷開,且不再接收新的連接),斷開和 Master 的 IPC 通道,不再接受新的用戶請求。
Master 立刻 fork 一個新的 Worker 進程,保證在線的『工人』總數不變。
異常 Worker 等待一段時間,處理完已經接受的請求后退出。
+---------+ +---------+ | Worker | | Master | +---------+ +----+----+ | uncaughtException | +------------+ | | | | +---------+ | <----------+ | | Worker | | | +----+----+ | disconnect | fork a new worker | +-------------------------> + ---------------------> | | wait... | | | exit | | +-------------------------> | | | | | die | | | | | |
當一個進程出現異常導致 crash 或者 OOM 被系統殺死時,不像未捕獲異常發生時我們還有機會讓進程繼續執行,只能夠讓當前進程直接退出,Master 立刻 fork 一個新的 Worker。
child_process 模塊提供了衍生子進程的能力, 簡單來說就是執行cmd命令的能力
。
默認情況下, stdin、 stdout 和 stderr 的管道會在父 Node.js 進程和衍生的子進程之間建立
。 這些管道具有有限的(且平臺特定的)容量。 如果子進程寫入 stdout 時超出該限制且沒有捕獲輸出,則子進程會阻塞并等待管道緩沖區接受更多的數據。 這與 shell 中的管道的行為相同。 如果不消費輸出,則使用 { stdio: 'ignore' } 選項。
const cp = require('child_process');
通過 API 創建出來的子進程和父進程沒有任何必然聯系
4個異步方法,創建子進程:fork、exec、execFile、spawn
spawn(command, args)
:處理一些會有很多子進程 I/O 時、進程會有大量輸出時使用
execFile(file, args[, callback])
:只需執行一個外部程序的時候使用,執行速度快,處理用戶輸入相對安全
exec(command, options)
:想直接訪問線程的 shell 命令時使用,一定要注意用戶輸入
fork(modulePath, args)
:想將一個 Node 進程作為一個獨立的進程來運行的時候使用,使得計算處理和文件描述器脫離 Node 主進程(復制一個子進程)
Node
非 Node
3個同步方法:execSync
、execFileSync
、spawnSync
其他三種方法都是 spawn()
的延伸。
fork 方法會開放一個 IPC 通道,不同的 Node 進程進行消息傳送
一個子進程消耗 30ms 啟動時間和 10MB 內存
記住,衍生的 Node.js 子進程獨立于父進程,但兩者之間建立的 IPC 通信通道除外。 每個進程都有自己的內存,帶有自己的 V8 實例
舉個?
在一個目錄下新建 worker.js 和 master.js 兩個文件:
# child.js const t = JSON.parse(process.argv[2]); console.error(`子進程 t=${JSON.stringify(t)}`); process.send({hello:`兒子pid=${process.pid} 給爸爸進程pid=${process.ppid} 請安`}); process.on('message', (msg)=>{ console.error(`子進程 msg=${JSON.stringify(msg)}`); });
# parent.js const {fork} = require('child_process'); for(let i = 0; i < 3; i++){ const p = fork('./child.js', [JSON.stringify({id:1,name:1})]); p.on('message', (msg) => { console.log(`messsgae from child msg=${JSON.stringify(msg)}`, ); }); p.send({hello:`來自爸爸${process.pid} 進程id=${i}的問候`}); }
通過 node parent.js
啟動 parent.js,然后通過 ps aux | grep worker.js
查看進程的數量,我們可以發現,理想狀況下,進程的數量等于 CPU 的核心數,每個進程各自利用一個 CPU 核心。
這是經典的 Master-Worker 模式(主從模式)
實際上,fork 進程是昂貴的,復制進程的目的是充分利用 CPU 資源,所以 NodeJS 在單線程上使用了事件驅動的方式來解決高并發的問題。
適用場景
一般用于比較耗時的場景,并且用node去實現的,比如下載文件;
fork可以實現多線程下載:將文件分成多塊,然后每個進程下載一部分,最后拼起來;
會把輸出結果緩存好,通過回調返回最后結果或者異常信息
const cp = require('child_process'); // 第一個參數,要運行的可執行文件的名稱或路徑。這里是echo cp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => { if (err) { console.error(err); } console.log('stdout: ', stdout); console.log('stderr: ', stderr); });
適用場景
比較適合開銷小的任務,更關注結果,比如ls等;
主要用來執行一個shell方法,其內部還是調用了spawn ,不過他有最大緩存限制。
只有一個字符串命令
和 shell 一模一樣
const cp = require('child_process'); cp.exec(`cat ${__dirname}/messy.txt | sort | uniq`, (err, stdout, stderr) => { console.log(stdout); });
適用場景
比較適合開銷小的任務,更關注結果,比如ls等;
通過流可以使用有大量數據輸出的外部應用,節約內存
使用流提高數據響應效率
spawn 方法返回一個 I/O 的流接口
單一任務
const cp = require('child_process'); const child = cp.spawn('echo', ['hello', 'world']); child.on('error', console.error); # 輸出是流,輸出到主進程stdout,控制臺 child.stdout.pipe(process.stdout); child.stderr.pipe(process.stderr);
多任務串聯
const cp = require('child_process'); const path = require('path'); const cat = cp.spawn('cat', [path.resolve(__dirname, 'messy.txt')]); const sort = cp.spawn('sort'); const uniq = cp.spawn('uniq'); # 輸出是流 cat.stdout.pipe(sort.stdin); sort.stdout.pipe(uniq.stdin); uniq.stdout.pipe(process.stdout);
適用場景
spawn是流式的,所以適合耗時任務,比如執行npm install,打印install的過程
在進程已結束并且子進程的標準輸入輸出流(sdtio)已關閉之后,則觸發 'close'
事件。這個事件跟exit
不同,因為多個進程可以共享同個stdio流。
參數:
code(退出碼,如果子進程是自己退出的話)
signal(結束子進程的信號)
問題:code一定是有的嗎?
(從對code的注解來看好像不是)比如用kill
殺死子進程,那么,code是?
參數:
code、signal,如果子進程是自己退出的,那么code
就是退出碼,否則為null;
如果子進程是通過信號結束的,那么,signal
就是結束進程的信號,否則為null。
這兩者中,一者肯定不為null。
注意事項:exit
事件觸發時,子進程的stdio stream可能還打開著。(場景?)此外,nodejs監聽了SIGINT和SIGTERM信號,也就是說,nodejs收到這兩個信號時,不會立刻退出,而是先做一些清理的工作,然后重新拋出這兩個信號。(目測此時js可以做清理工作了,比如關閉數據庫等。)
SIGINT
:interrupt,程序終止信號,通常在用戶按下CTRL+C時發出,用來通知前臺進程終止進程。SIGTERM
:terminate,程序結束信號,該信號可以被阻塞和處理,通常用來要求程序自己正常退出。shell命令kill缺省產生這個信號。如果信號終止不了,我們才會嘗試SIGKILL(強制終止)。
當發生下列事情時,error就會被觸發。當error觸發時,exit可能觸發,也可能不觸發。(內心是崩潰的)
無法衍生該進程。
進程無法kill。
向子進程發送消息失敗。
當采用process.send()
來發送消息時觸發。
參數:message
,為json對象,或者primitive value;sendHandle
,net.Socket對象,或者net.Server對象(熟悉cluster的同學應該對這個不陌生)
.connected:當調用.disconnected()
時,設為false。代表是否能夠從子進程接收消息,或者對子進程發送消息。
.disconnect() :關閉父進程、子進程之間的IPC通道。當這個方法被調用時,disconnect
事件就會觸發。如果子進程是node實例(通過child_process.fork()創建),那么在子進程內部也可以主動調用process.disconnect()
來終止IPC通道。
應對單線程問題,通常使用多進程的方式來模擬多線程
對 cpu 利用不足
某個未捕獲的異常可能會導致整個程序的退出
Node 進程占用了 7 個線程
Node 中最核心的是 v8 引擎,在 Node 啟動后,會創建 v8 的實例,這個實例是多線程的
主線程:編譯、執行代碼
編譯/優化線程:在主線程執行的時候,可以優化代碼
分析器線程:記錄分析代碼運行時間,為 Crankshaft 優化代碼執行提供依據
垃圾回收的幾個線程
JavaScript 的執行是單線程
的,但 Javascript 的宿主環境,無論是 Node 還是瀏覽器都是多線程的。
Javascript 為什么是單線程?
這個問題需要從瀏覽器說起,在瀏覽器環境中對于 DOM 的操作,試想如果多個線程來對同一個 DOM 操作是不是就亂了呢,那也就意味著對于DOM的操作只能是單線程,避免 DOM 渲染沖突。在瀏覽器環境中 UI 渲染線程和 JS 執行引擎是互斥的,一方在執行時都會導致另一方被掛起,這是由 JS 引擎所決定的。
Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集計算(Zlib,Crypto)會啟用 Node 的線程池
線程池默認大小為 4,可以手動更改線程池默認大小
process.env.UV_THREADPOOL_SIZE = 64
Node 10.5.0 的發布,給出了一個實驗性質的模塊 worker_threads
給 Node 提供真正的多線程能力
worker_thread 模塊中有 4 個對象和 2 個類
isMainThread: 是否是主線程,源碼中是通過 threadId === 0 進行判斷的。
MessagePort: 用于線程之間的通信,繼承自 EventEmitter。
MessageChannel: 用于創建異步、雙向通信的通道實例。
threadId: 線程 ID。
Worker: 用于在主線程中創建子線程。第一個參數為 filename,表示子線程執行的入口。
parentPort: 在 worker 線程里是表示父進程的 MessagePort 類型的對象,在主線程里為 null
workerData: 用于在主進程中向子進程傳遞數據(data 副本)
const { isMainThread, parentPort, workerData, threadId, MessageChannel, MessagePort, Worker } = require('worker_threads'); function mainThread() { for (let i = 0; i < 5; i++) { const worker = new Worker(__filename, { workerData: i }); worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); }); worker.on('message', msg => { console.log(`main: receive ${msg}`); worker.postMessage(msg + 1); }); } } function workerThread() { console.log(`worker: workerDate ${workerData}`); parentPort.on('message', msg => { console.log(`worker: receive ${msg}`); }), parentPort.postMessage(workerData); } if (isMainThread) { mainThread(); } else { workerThread(); }
const assert = require('assert'); const { Worker, MessageChannel, MessagePort, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { const worker = new Worker(__filename); const subChannel = new MessageChannel(); worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]); subChannel.port2.on('message', (value) => { console.log('received:', value); }); } else { parentPort.once('message', (value) => { assert(value.hereIsYourPort instanceof MessagePort); value.hereIsYourPort.postMessage('the worker is sending this'); value.hereIsYourPort.close(); }); }
進程是資源分配的最小單位,線程是CPU調度的最小單位
IPC (Inter-process communication) 即進程間通信
,由于每個進程創建之后都有自己的獨立地址空間,實現 IPC 的目的就是為了進程之間資源共享訪問。
實現 IPC 的方式有多種:管道、消息隊列、信號量、Domain Socket,Node.js 通過 pipe 來實現。
實際上,父進程會在創建子進程之前,會先創建 IPC 通道并監聽這個 IPC,然后再創建子進程,通過環境變量(NODE_CHANNEL_FD)告訴子進程和 IPC 通道相關的文件描述符,子進程啟動的時候根據文件描述符連接 IPC 通道,從而和父進程建立連接。
句柄是一種可以用來標識資源的引用的,它的內部包含了指向對象的文件資源描述符。
一般情況下,當我們想要將多個進程監聽到一個端口下,可能會考慮使用主進程代理的方式處理:
然而,這種代理方案會導致每次請求的接收和代理轉發用掉兩個文件描述符,而系統的文件描述符是有限的,這種方式會影響系統的擴展能力。
所以,為什么要使用句柄?原因是在實際應用場景下,建立 IPC 通信后可能會涉及到比較復雜的數據處理場景,句柄可以作為 send()
方法的第二個可選參數傳入,也就是說可以直接將資源的標識通過 IPC 傳輸,避免了上面所說的代理轉發造成的文件描述符的使用。
以下是支持發送的句柄類型:
net.Socket
net.Server
net.Native
dgram.Socket
dgram.Native
父進程創建子進程之后,父進程退出了,但是父進程對應的一個或多個子進程還在運行,這些子進程會被系統的 init 進程收養,對應的進程 ppid 為 1,這就是孤兒進程。通過以下代碼示例說明。
# worker.js const http = require('http'); const server = http.createServer((req, res) => { res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); // 記錄當前工作進程 pid 及父進程 ppid }); let worker; process.on('message', function (message, sendHandle) { if (message === 'server') { worker = sendHandle; worker.on('connection', function(socket) { server.emit('connection', socket); }); } });
# master.js const fork = require('child_process').fork; const server = require('net').createServer(); server.listen(3000); const worker = fork('worker.js'); worker.send('server', server); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); process.exit(0); // 創建子進程之后,主進程退出,此時創建的 worker 進程會成為孤兒進程
控制臺進行測試,輸出當前工作進程 pid 和 父進程 ppid
由于在 master.js 里退出了父進程,活動監視器所顯示的也就只有工作進程。
再次驗證,打開控制臺調用接口,可以看到工作進程 5611 對應的 ppid 為 1(為 init 進程),此時已經成為了孤兒進程
守護進程運行在后臺不受終端的影響,什么意思呢?
Node.js 開發的同學們可能熟悉,當我們打開終端執行 node app.js
開啟一個服務進程之后,這個終端就會一直被占用,如果關掉終端,服務就會斷掉,即前臺運行模式
。
如果采用守護進程進程方式,這個終端我執行 node app.js
開啟一個服務進程之后,我還可以在這個終端上做些別的事情,且不會相互影響。
創建子進程
在子進程中創建新會話(調用系統函數 setsid)
改變子進程工作目錄(如:“/” 或 “/usr/ 等)
父進程終止
index.js 文件里的處理邏輯使用 spawn 創建子進程完成了上面的第一步操作。
設置 options.detached
為 true 可以使子進程在父進程退出后繼續運行(系統層會調用 setsid 方法),這是第二步操作。
options.cwd 指定當前子進程工作目錄若不做設置默認繼承當前工作目錄,這是第三步操作。
運行 daemon.unref() 退出父進程,這是第四步操作。
// index.js const spawn = require('child_process').spawn; function startDaemon() { const daemon = spawn('node', ['daemon.js'], { cwd: '/usr', detached : true, stdio: 'ignore', }); console.log('守護進程開啟 父進程 pid: %s, 守護進程 pid: %s', process.pid, daemon.pid); daemon.unref(); } startDaemon()
daemon.js 文件里處理邏輯開啟一個定時器每 10 秒執行一次,使得這個資源不會退出,同時寫入日志到子進程當前工作目錄下
/usr/daemon.js const fs = require('fs'); const { Console } = require('console'); // custom simple logger const logger = new Console(fs.createWriteStream('./stdout.log'), fs.createWriteStream('./stderr.log')); setInterval(function() { logger.log('daemon pid: ', process.pid, ', ppid: ', process.ppid); }, 1000 * 10);
在實際工作中對于守護進程并不陌生,例如 PM2、Egg-Cluster 等,以上只是一個簡單的 Demo 對守護進程做了一個說明,在實際工作中對守護進程的健壯性要求還是很高的,例如:進程的異常監聽、工作進程管理調度、進程掛掉之后重啟等等,這些還需要去不斷思考。
目錄是什么?
進程的當前工作目錄可以通過 process.cwd()
命令獲取,默認為當前啟動的目錄,如果是創建子進程則繼承于父進程的目錄,可通過 process.chdir()
命令重置,例如通過 spawn 命令創建的子進程可以指定 cwd 選項設置子進程的工作目錄。
有什么作用?
例如,通過 fs 讀取文件,如果設置為相對路徑則相對于當前進程啟動的目錄進行查找,所以,啟動目錄設置有誤的情況下將無法得到正確的結果。還有一種情況程序里引用第三方模塊也是根據當前進程啟動的目錄來進行查找的。
// 示例 process.chdir('/Users/may/Documents/test/') // 設置當前進程目錄 console.log(process.cwd()); // 獲取當前進程目錄
到此,關于“NodeJS中的進程管理怎么實現”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。