經過對如下 10 個面試題的分享,助您更好的理解 Node.js 的進程和線程相關知識html
做者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 後青年,公衆號 「Nodejs技術棧」,Github 開源項目 www.nodejs.rednode
app.listen(port)
在進行 fork 時,爲何沒有報端口被佔用?參考:Interview3做者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 後青年,公衆號 「Nodejs技術棧」,Github 開源項目 www.nodejs.redgit
什麼是進程和線程?之間的區別?github
關於線程和進程是服務端一個很基礎的概念,在文章 Node.js進階之進程與線程 中介紹了進程與線程的概念以後又給出了在 Node.js 中的進程和線程的實際應用,對於這塊不是很理解的建議先看下。面試
什麼是孤兒進程?數據庫
父進程建立子進程以後,父進程退出了,可是父進程對應的一個或多個子進程還在運行,這些子進程會被系統的 init 進程收養,對應的進程 ppid 爲 1,這就是孤兒進程。經過如下代碼示例說明。api
// 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 進程會成爲孤兒進程
複製代碼
// 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);
});
}
});
複製代碼
孤兒進程 示例源碼瀏覽器
控制檯進行測試,輸出當前工做進程 pid 和 父進程 ppidbash
$ node master
worker process created, pid: 32971 ppid: 32970
複製代碼
因爲在 master.js 裏退出了父進程,活動監視器所顯示的也就只有工做進程。服務器
再次驗證,打開控制檯調用接口,能夠看到工做進程 32971 對應的 ppid 爲 1(爲 init 進程),此時已經成爲了孤兒進程
$ curl http://127.0.0.1:3000
I am worker, pid: 32971, ppid: 1
複製代碼
建立多進程時,代碼裏有
app.listen(port)
在進行 fork 時,爲何沒有報端口被佔用?
先看下端口被佔用的狀況
// 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
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);
});
}
});
複製代碼
驗證一番,控制檯執行 node master.js
如下結果是咱們預期的,多進程端口占用問題已經被解決了。
$ 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
複製代碼
關於多進程端口占用問題,cnode 上有篇文章也能夠看下 經過源碼解析 Node.js 中 cluster 模塊的主要功能實現
什麼是 IPC 通訊,如何創建 IPC 通訊?什麼場景下須要用到 IPC 通訊?
IPC (Inter-process communication) ,即進程間通訊技術,因爲每一個進程建立以後都有本身的獨立地址空間,實現 IPC 的目的就是爲了進程之間資源共享訪問,實現 IPC 的方式有多種:管道、消息隊列、信號量、Domain Socket,Node.js 經過 pipe 來實現。
看一下 Demo,未使用 IPC 的狀況
// pipe.js
const spawn = require('child_process').spawn;
const child = spawn('node', ['worker.js'])
console.log(process.pid, child.pid); // 主進程id3243 子進程3244
複製代碼
// worker.js
console.log('I am worker, PID: ', process.pid);
複製代碼
控制檯執行 node pipe.js
,輸出主進程id、子進程id,可是子進程 worker.js
的信息並無在控制檯打印,緣由是新建立的子進程有本身的stdio 流。
$ node pipe.js
41948 41949
複製代碼
建立一個父進程和子進程之間傳遞消息的 IPC 通道實現輸出信息
修改 pipe.js 讓子進程的 stdio 和當前進程的 stdio 之間創建管道連接,還能夠經過 spawn() 方法的 stdio 選項創建 IPC 機制,參考 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);
複製代碼
再次驗證,控制檯執行 node pipe.js
,worker.js 的信息也打印了出來
$ 42473 42474
I am worker, PID: 42474
複製代碼
關於父進程與子進程是如何通訊的?
參考了深刻淺出 Node.js 一書,父進程在建立子進程以前會先去建立 IPC 通道並一直監聽該通道,以後開始建立子進程並經過環境變量(NODE_CHANNEL_FD)的方式將 IPC 頻道的文件描述符傳遞給子進程,子進程啓動時根據傳遞的文件描述符去連接 IPC 通道,從而創建父子進程之間的通訊機制。
父子進程 IPC 通訊交互圖
Node.js 是單線程仍是多線程?進一步會提問爲何是單線程?
第一個問題,Node.js 是單線程仍是多線程?這個問題是個基本的問題,在以往面試中偶爾提到仍是有不知道的,Javascript 是單線程的,可是作爲其在服務端運行環境的 Node.js 並不是是單線程的。
第二個問題,Javascript 爲何是單線程?這個問題須要從瀏覽器提及,在瀏覽器環境中對於 DOM 的操做,試想若是多個線程來對同一個 DOM 操做是否是就亂了呢,那也就意味着對於DOM的操做只能是單線程,避免 DOM 渲染衝突。在瀏覽器環境中 UI 渲染線程和 JS 執行引擎是互斥的,一方在執行時都會致使另外一方被掛起,這是由 JS 引擎所決定的。
關於守護進程,是什麼、爲何、怎麼編寫?
守護進程運行在後臺不受終端的影響,什麼意思呢?Node.js 開發的同窗們可能熟悉,當咱們打開終端執行 node app.js
開啓一個服務進程以後,這個終端就會一直被佔用,若是關掉終端,服務就會斷掉,即前臺運行模式。若是採用守護進程進程方式,這個終端我執行 node app.js
開啓一個服務進程以後,我還能夠在這個終端上作些別的事情,且不會相互影響。
建立步驟
Node.js 編寫守護進程 Demo 展現
index.js 文件裏的處理邏輯使用 spawn 建立子進程完成了上面的第一步操做。設置 options.detached 爲 true 可使子進程在父進程退出後繼續運行(系統層會調用 setsid 方法),參考 options_detached,這是第二步操做。options.cwd 指定當前子進程工做目錄若不作設置默認繼承當前工做目錄,這是第三步操做。運行 daemon.unref() 退出父進程,參考 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('守護進程開啓 父進程 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);
複製代碼
運行測試
$ node index.js
守護進程開啓 父進程 pid: 47608, 守護進程 pid: 47609
複製代碼
打開活動監視器查看,目前只有一個進程 47609,這就是咱們須要進行守護的進程
守護進程閱讀推薦
守護進程總結
在實際工做中對於守護進程並不陌生,例如 PM二、Egg-Cluster 等,以上只是一個簡單的 Demo 對守護進程作了一個說明,在實際工做中對守護進程的健壯性要求仍是很高的,例如:進程的異常監聽、工做進程管理調度、進程掛掉以後重啓等等,這些還須要咱們去不斷思考。
採用子進程 child_process 的 spawn 方法,以下所示:
const spawn = require('child_process').spawn;
const child = spawn('echo', ["簡單的命令行交互"]);
child.stdout.pipe(process.stdout); // 將子進程的輸出作爲當前進程的輸入,打印在控制檯
複製代碼
$ node execfile
簡單的命令行交互
複製代碼
如何讓一個 js 文件在 Linux 下成爲一個可執行命令程序?
#!/usr/bin/env node
,表示當前腳本使用 Node.js 進行解析sudo ln -s /${dir}/hello.js /usr/local/bin/hello
,文件名就是咱們在終端使用的名字#!/usr/bin/env node
console.log('hello world!');
複製代碼
終端測試
$ hello
hello world!
複製代碼
進程的當前工做目錄是什麼? 有什麼做用?
進程的當前工做目錄能夠經過 process.cwd() 命令獲取,默認爲當前啓動的目錄,若是是建立子進程則繼承於父進程的目錄,可經過 process.chdir() 命令重置,例如經過 spawn 命令建立的子進程能夠指定 cwd 選項設置子進程的工做目錄。
有什麼做用?例如,經過 fs 讀取文件,若是設置爲相對路徑則相對於當前進程啓動的目錄進行查找,因此,啓動目錄設置有誤的狀況下將沒法獲得正確的結果。還有一種狀況程序裏引用第三方模塊也是根據當前進程啓動的目錄來進行查找的。
// 示例
process.chdir('/Users/may/Documents/test/') // 設置當前進程目錄
console.log(process.cwd()); // 獲取當前進程目錄
複製代碼
多進程或多個 Web 服務之間的狀態共享問題?
多進程模式下各個進程之間是相互獨立的,例如用戶登錄以後 session 的保存,若是保存在服務進程裏,那麼若是我有 4 個工做進程,每一個進程都要保存一份這是不必的,假設服務重啓了數據也會丟失。多個 Web 服務也是同樣的,還會出現我在 A 機器上建立了 Session,當負載均衡分發到 B 機器上以後還須要在建立一份。通常的作法是經過 Redis 或者 數據庫來作數據共享。
首發於慕課網,www.imooc.com/article/288…