通過對(duì)以下 10 個(gè)面試題的分享,助您更好的理解 Node.js 的進(jìn)程和線程相關(guān)知識(shí)
快速導(dǎo)航
- 什么是進(jìn)程和線程?之間的區(qū)別?參考:Interview1
- 什么是孤兒進(jìn)程寿羞?參考:Interview2
- 創(chuàng)建多進(jìn)程時(shí)剥扣,代碼里有
app.listen(port)
在進(jìn)行 fork 時(shí)打厘,為什么沒有報(bào)端口被占用?參考:Interview3 - 什么是 IPC 通信蓖扑,如何建立 IPC 通信虱岂?什么場景下需要用到 IPC 通信玖院?參考:Interview4
- Node.js 是單線程還是多線程?進(jìn)一步會(huì)提問為什么是單線程第岖?參考:Interview5
- 關(guān)于守護(hù)進(jìn)程难菌,是什么、為什么蔑滓、怎么編寫郊酒?參考:Interview6
- 實(shí)現(xiàn)一個(gè)簡單的命令行交互程序?參考:Interview7
- 如何讓一個(gè) js 文件在 Linux 下成為一個(gè)可執(zhí)行命令程序键袱?參考:Interview8
- 進(jìn)程的當(dāng)前工作目錄是什么? 有什么作用燎窘?參考:Interview9
- 多進(jìn)程或多個(gè) Web 服務(wù)之間的狀態(tài)共享問題?參考:Interview10
作者簡介:五月君蹄咖,Nodejs Developer褐健,熱愛技術(shù)、喜歡分享的 90 后青年比藻,公眾號(hào) “Nodejs技術(shù)椔亮浚”,Github 開源項(xiàng)目 https://www.nodejs.red
Interview1
什么是進(jìn)程和線程银亲?之間的區(qū)別?
關(guān)于線程和進(jìn)程是服務(wù)端一個(gè)很基礎(chǔ)的概念纽匙,在文章 Node.js進(jìn)階之進(jìn)程與線程 中介紹了進(jìn)程與線程的概念之后又給出了在 Node.js 中的進(jìn)程和線程的實(shí)際應(yīng)用务蝠,對(duì)于這塊不是很理解的建議先看下。
Interview2
什么是孤兒進(jìn)程烛缔?
父進(jìn)程創(chuàng)建子進(jìn)程之后馏段,父進(jìn)程退出了,但是父進(jìn)程對(duì)應(yīng)的一個(gè)或多個(gè)子進(jìn)程還在運(yùn)行践瓷,這些子進(jìn)程會(huì)被系統(tǒng)的 init 進(jìn)程收養(yǎng)院喜,對(duì)應(yīng)的進(jìn)程 ppid 為 1,這就是孤兒進(jìn)程晕翠。通過以下代碼示例說明喷舀。
// 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); // 創(chuàng)建子進(jìn)程之后砍濒,主進(jìn)程退出,此時(shí)創(chuàng)建的 worker 進(jìn)程會(huì)成為孤兒進(jìn)程
// worker.js
const http = require('http');
const server = http.createServer((req, res) => {
res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); // 記錄當(dāng)前工作進(jìn)程 pid 及父進(jìn)程 ppid
});
let worker;
process.on('message', function (message, sendHandle) {
if (message === 'server') {
worker = sendHandle;
worker.on('connection', function(socket) {
server.emit('connection', socket);
});
}
});
控制臺(tái)進(jìn)行測試硫麻,輸出當(dāng)前工作進(jìn)程 pid 和 父進(jìn)程 ppid
$ node master
worker process created, pid: 32971 ppid: 32970
由于在 master.js 里退出了父進(jìn)程爸邢,活動(dòng)監(jiān)視器所顯示的也就只有工作進(jìn)程。
[圖片上傳失敗...(image-1cfbab-1560814309268)]
再次驗(yàn)證拿愧,打開控制臺(tái)調(diào)用接口杠河,可以看到工作進(jìn)程 32971 對(duì)應(yīng)的 ppid 為 1(為 init 進(jìn)程),此時(shí)已經(jīng)成為了孤兒進(jìn)程
$ curl http://127.0.0.1:3000
I am worker, pid: 32971, ppid: 1
Interview3
創(chuàng)建多進(jìn)程時(shí)浇辜,代碼里有
app.listen(port)
在進(jìn)行 fork 時(shí)券敌,為什么沒有報(bào)端口被占用?
先看下端口被占用的情況
// 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);
以上代碼示例柳洋,控制臺(tái)執(zhí)行 node master.js
只有一個(gè) worker 可以監(jiān)聽到 3000 端口陪白,其余將會(huì)拋出 Error: listen EADDRINUSE :::3000
錯(cuò)誤
那么多進(jìn)程模式下怎么實(shí)現(xiàn)多端口監(jiān)聽呢?答案還是有的膳灶,通過句柄傳遞 Node.js v0.5.9 版本之后支持進(jìn)程間可發(fā)送句柄功能咱士,怎么發(fā)送?如下所示:
/**
* http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
* message
* sendHandle
*/
subprocess.send(message, sendHandle)
當(dāng)父子進(jìn)程之間建立 IPC 通道之后轧钓,通過子進(jìn)程對(duì)象的 send 方法發(fā)送消息序厉,第二個(gè)參數(shù) sendHandle 就是句柄,可以是 TCP套接字毕箍、TCP服務(wù)器弛房、UDP套接字等,為了解決上面多進(jìn)程端口占用問題而柑,我們將主進(jìn)程的 socket 傳遞到子進(jìn)程文捶,修改代碼,如下所示:
//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
const http = require('http');
http.createServer((req, res) => {
res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);
})
let worker;
process.title = 'node-worker'
process.on('message', function (message, sendHandle) {
if (message === 'server') {
worker = sendHandle;
worker.on('connection', function(socket) {
server.emit('connection', socket);
});
}
});
驗(yàn)證一番媒咳,控制臺(tái)執(zhí)行 node master.js
以下結(jié)果是我們預(yù)期的粹排,多進(jìn)程端口占用問題已經(jīng)被解決了。
$ node master.js
worker process created, pid: 34512 ppid: 34511
worker process created, pid: 34513 ppid: 34511
worker process created, pid: 34514 ppid: 34511
worker process created, pid: 34515 ppid: 34511
關(guān)于多進(jìn)程端口占用問題涩澡,cnode 上有篇文章也可以看下 通過源碼解析 Node.js 中 cluster 模塊的主要功能實(shí)現(xiàn)
Interview4
什么是 IPC 通信顽耳,如何建立 IPC 通信?什么場景下需要用到 IPC 通信妙同?
IPC (Inter-process communication) 射富,即進(jìn)程間通信技術(shù),由于每個(gè)進(jìn)程創(chuàng)建之后都有自己的獨(dú)立地址空間粥帚,實(shí)現(xiàn) IPC 的目的就是為了進(jìn)程之間資源共享訪問胰耗,實(shí)現(xiàn) IPC 的方式有多種:管道、消息隊(duì)列芒涡、信號(hào)量柴灯、Domain Socket卖漫,Node.js 通過 pipe 來實(shí)現(xiàn)。
看一下 Demo弛槐,未使用 IPC 的情況
// pipe.js
const spawn = require('child_process').spawn;
const child = spawn('node', ['worker.js'])
console.log(process.pid, child.pid); // 主進(jìn)程id3243 子進(jìn)程3244
// worker.js
console.log('I am worker, PID: ', process.pid);
控制臺(tái)執(zhí)行 node pipe.js
懊亡,輸出主進(jìn)程id、子進(jìn)程id乎串,但是子進(jìn)程 worker.js
的信息并沒有在控制臺(tái)打印店枣,原因是新創(chuàng)建的子進(jìn)程有自己的stdio 流。
$ node pipe.js
41948 41949
創(chuàng)建一個(gè)父進(jìn)程和子進(jìn)程之間傳遞消息的 IPC 通道實(shí)現(xiàn)輸出信息
修改 pipe.js 讓子進(jìn)程的 stdio 和當(dāng)前進(jìn)程的 stdio 之間建立管道鏈接叹誉,還可以通過 spawn() 方法的 stdio 選項(xiàng)建立 IPC 機(jī)制鸯两,參考 options.stdio
// pipe.js
const spawn = require('child_process').spawn;
const child = spawn('node', ['worker.js'])
child.stdout.pipe(process.stdout);
console.log(process.pid, child.pid);
再次驗(yàn)證,控制臺(tái)執(zhí)行 node pipe.js
长豁,worker.js 的信息也打印了出來
$ 42473 42474
I am worker, PID: 42474
關(guān)于父進(jìn)程與子進(jìn)程是如何通信的钧唐?
參考了深入淺出 Node.js 一書,父進(jìn)程在創(chuàng)建子進(jìn)程之前會(huì)先去創(chuàng)建 IPC 通道并一直監(jiān)聽該通道匠襟,之后開始創(chuàng)建子進(jìn)程并通過環(huán)境變量(NODE_CHANNEL_FD)的方式將 IPC 頻道的文件描述符傳遞給子進(jìn)程钝侠,子進(jìn)程啟動(dòng)時(shí)根據(jù)傳遞的文件描述符去鏈接 IPC 通道,從而建立父子進(jìn)程之間的通信機(jī)制酸舍。
[圖片上傳失敗...(image-ed7fa4-1560814309268)]
<p style="text-align:center; padding: 10px;">父子進(jìn)程 IPC 通信交互圖</p>
Interview5
Node.js 是單線程還是多線程帅韧?進(jìn)一步會(huì)提問為什么是單線程?
第一個(gè)問題啃勉,Node.js 是單線程還是多線程忽舟?這個(gè)問題是個(gè)基本的問題,在以往面試中偶爾提到還是有不知道的淮阐,Javascript 是單線程的叮阅,但是做為其在服務(wù)端運(yùn)行環(huán)境的 Node.js 并非是單線程的。
第二個(gè)問題泣特,Javascript 為什么是單線程浩姥?這個(gè)問題需要從瀏覽器說起,在瀏覽器環(huán)境中對(duì)于 DOM 的操作群扶,試想如果多個(gè)線程來對(duì)同一個(gè) DOM 操作是不是就亂了呢及刻,那也就意味著對(duì)于DOM的操作只能是單線程,避免 DOM 渲染沖突竞阐。在瀏覽器環(huán)境中 UI 渲染線程和 JS 執(zhí)行引擎是互斥的,一方在執(zhí)行時(shí)都會(huì)導(dǎo)致另一方被掛起暑劝,這是由 JS 引擎所決定的骆莹。
Interview6
關(guān)于守護(hù)進(jìn)程,是什么担猛、為什么幕垦、怎么編寫丢氢?
守護(hù)進(jìn)程運(yùn)行在后臺(tái)不受終端的影響,什么意思呢先改?Node.js 開發(fā)的同學(xué)們可能熟悉疚察,當(dāng)我們打開終端執(zhí)行 node app.js
開啟一個(gè)服務(wù)進(jìn)程之后,這個(gè)終端就會(huì)一直被占用仇奶,如果關(guān)掉終端貌嫡,服務(wù)就會(huì)斷掉,即前臺(tái)運(yùn)行模式该溯。如果采用守護(hù)進(jìn)程進(jìn)程方式岛抄,這個(gè)終端我執(zhí)行 node app.js
開啟一個(gè)服務(wù)進(jìn)程之后,我還可以在這個(gè)終端上做些別的事情狈茉,且不會(huì)相互影響夫椭。
創(chuàng)建步驟
- 創(chuàng)建子進(jìn)程
- 在子進(jìn)程中創(chuàng)建新會(huì)話(調(diào)用系統(tǒng)函數(shù) setsid)
- 改變子進(jìn)程工作目錄(如:“/” 或 “/usr/ 等)
- 父進(jìn)程終止
Node.js 編寫守護(hù)進(jìn)程 Demo 展示
index.js 文件里的處理邏輯使用 spawn 創(chuàng)建子進(jìn)程完成了上面的第一步操作。設(shè)置 options.detached 為 true 可以使子進(jìn)程在父進(jìn)程退出后繼續(xù)運(yùn)行(系統(tǒng)層會(huì)調(diào)用 setsid 方法)氯庆,參考 options_detached蹭秋,這是第二步操作。options.cwd 指定當(dāng)前子進(jìn)程工作目錄若不做設(shè)置默認(rèn)繼承當(dāng)前工作目錄堤撵,這是第三步操作仁讨。運(yùn)行 daemon.unref() 退出父進(jìn)程,參考 options.stdio粒督,這是第四步操作陪竿。
// index.js
const spawn = require('child_process').spawn;
function startDaemon() {
const daemon = spawn('node', ['daemon.js'], {
cwd: '/usr',
detached : true,
stdio: 'ignore',
});
console.log('守護(hù)進(jìn)程開啟 父進(jìn)程 pid: %s, 守護(hù)進(jìn)程 pid: %s', process.pid, daemon.pid);
daemon.unref();
}
startDaemon()
daemon.js 文件里處理邏輯開啟一個(gè)定時(shí)器每 10 秒執(zhí)行一次,使得這個(gè)資源不會(huì)退出屠橄,同時(shí)寫入日志到子進(jìn)程當(dāng)前工作目錄下
// /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);
守護(hù)進(jìn)程實(shí)現(xiàn) Node.js 版本 源碼地址
運(yùn)行測試
$ node index.js
守護(hù)進(jìn)程開啟 父進(jìn)程 pid: 47608, 守護(hù)進(jìn)程 pid: 47609
打開活動(dòng)監(jiān)視器查看族跛,目前只有一個(gè)進(jìn)程 47609,這就是我們需要進(jìn)行守護(hù)的進(jìn)程
[圖片上傳失敗...(image-7c95d-1560814309268)]
守護(hù)進(jìn)程閱讀推薦
守護(hù)進(jìn)程總結(jié)
在實(shí)際工作中對(duì)于守護(hù)進(jìn)程并不陌生锐墙,例如 PM2礁哄、Egg-Cluster 等,以上只是一個(gè)簡單的 Demo 對(duì)守護(hù)進(jìn)程做了一個(gè)說明溪北,在實(shí)際工作中對(duì)守護(hù)進(jìn)程的健壯性要求還是很高的桐绒,例如:進(jìn)程的異常監(jiān)聽、工作進(jìn)程管理調(diào)度之拨、進(jìn)程掛掉之后重啟等等茉继,這些還需要我們?nèi)ゲ粩嗨伎肌?/p>
Interview7
采用子進(jìn)程 child_process 的 spawn 方法,如下所示:
const spawn = require('child_process').spawn;
const child = spawn('echo', ["簡單的命令行交互"]);
child.stdout.pipe(process.stdout); // 將子進(jìn)程的輸出做為當(dāng)前進(jìn)程的輸入蚀乔,打印在控制臺(tái)
$ node execfile
簡單的命令行交互
Interview8
如何讓一個(gè) js 文件在 Linux 下成為一個(gè)可執(zhí)行命令程序?
- 新建 hello.js 文件烁竭,頭部須加上
#!/usr/bin/env node
,表示當(dāng)前腳本使用 Node.js 進(jìn)行解析 - 賦予文件可執(zhí)行權(quán)限 chmod +x chmod +x /${dir}/hello.js吉挣,目錄自定義
- 在 /usr/local/bin 目錄下創(chuàng)建一個(gè)軟鏈文件
sudo ln -s /${dir}/hello.js /usr/local/bin/hello
派撕,文件名就是我們在終端使用的名字 - 終端執(zhí)行 hello 相當(dāng)于輸入 node hello.js
#!/usr/bin/env node
console.log('hello world!');
終端測試
$ hello
hello world!
Interview9
進(jìn)程的當(dāng)前工作目錄是什么? 有什么作用?
進(jìn)程的當(dāng)前工作目錄可以通過 process.cwd() 命令獲取婉弹,默認(rèn)為當(dāng)前啟動(dòng)的目錄,如果是創(chuàng)建子進(jìn)程則繼承于父進(jìn)程的目錄终吼,可通過 process.chdir() 命令重置镀赌,例如通過 spawn 命令創(chuàng)建的子進(jìn)程可以指定 cwd 選項(xiàng)設(shè)置子進(jìn)程的工作目錄。
有什么作用际跪?例如商佛,通過 fs 讀取文件,如果設(shè)置為相對(duì)路徑則相對(duì)于當(dāng)前進(jìn)程啟動(dòng)的目錄進(jìn)行查找垫卤,所以威彰,啟動(dòng)目錄設(shè)置有誤的情況下將無法得到正確的結(jié)果。還有一種情況程序里引用第三方模塊也是根據(jù)當(dāng)前進(jìn)程啟動(dòng)的目錄來進(jìn)行查找的穴肘。
// 示例
process.chdir('/Users/may/Documents/test/') // 設(shè)置當(dāng)前進(jìn)程目錄
console.log(process.cwd()); // 獲取當(dāng)前進(jìn)程目錄
Interview10
多進(jìn)程或多個(gè) Web 服務(wù)之間的狀態(tài)共享問題歇盼?
多進(jìn)程模式下各個(gè)進(jìn)程之間是相互獨(dú)立的,例如用戶登陸之后 session 的保存评抚,如果保存在服務(wù)進(jìn)程里豹缀,那么如果我有 4 個(gè)工作進(jìn)程,每個(gè)進(jìn)程都要保存一份這是沒必要的慨代,假設(shè)服務(wù)重啟了數(shù)據(jù)也會(huì)丟失邢笙。多個(gè) Web 服務(wù)也是一樣的,還會(huì)出現(xiàn)我在 A 機(jī)器上創(chuàng)建了 Session侍匙,當(dāng)負(fù)載均衡分發(fā)到 B 機(jī)器上之后還需要在創(chuàng)建一份氮惯。一般的做法是通過 Redis 或者 數(shù)據(jù)庫來做數(shù)據(jù)共享。