在node工程部署中,經常涉及到三方:本地客戶端、跳板機和服務器(集羣)。在經過git觸發gitlab hook腳本後,須要在跳板機中執行相應的ssh命令執行shell文件啓動node服務器,這須要使用一個經常使用的命令setsid,這樣當ssh命令執行完畢shell退出後,node服務器仍正常運行,此時node服務進程就是一個最典型的daemon進程(後臺服務進程)。node
那麼,在node項目中,如何建立一個daemon進程呢?最簡單的方式,其實就是採用相似上文中介紹的方式:linux
require('child_process').exec('setsid node app.js >/dev/null 2>&1 &');
這樣能夠經過執行shell的方式實現daemon進程。不過本文的重點並非介紹這種「命令行」的方式實現daemon進程,並且本文會詳細講述daemon進程的建立原理,且看下文。git
在當前業務中,之因此須要建立daemon進程就是爲了保證中斷建立該進程的父進程(ctrl+c)或者父進程執行完畢後並不影響daemon進程的執行。下文介紹兩種實現方式,實現原理細節上有些出入。shell
下文中的全部討論都是在linux環境下進行。服務器
在linux系統中,父進程建立出子進程,此時父進程若退出,此時子進程則變爲孤兒進程,其ppid變爲1,即成爲init進程的子進程。在node環境下,若是不針對子進程的stdio作一些特殊處理父進程其實不會真正退出,而是直到子進程執行完畢後再退出。之因此出現這種狀況是因爲node建立子進程時默認會經過pipe方式將子進程的輸出導流到父進程的stream中(childProcess.stdout、childProcess.stderr),提供在父進程中輸出子進程消息的能力。session
所以,解決此種問題可給子進程的stdio從新賦值:app
file: parent.js let cp = require('child_process'); const sp = cp.spawn('node',['./c.js'],{ stdio: [process.stdin,process.stdout,process.stderr] }); setTimeout(()=>{console.log('parent out')},5000); -------------- file: c.js setTimeout(()=>{ console.log('children exit'); },10000)
經過在parent.js中設置子進程的stdio爲當前終端(其實繼承了父進程的stdio),這樣父進程在5s後退出,此時子進程的ppid變爲1,10s後子進程退出。ssh
上述實現只知足「父進程正常退出,子進程成爲守護進程」的狀況,一旦經過「ctrl+c」的方式終端父進程,子進程仍會退出,這仍是與node底層實現有關。默認「ctrl+c」觸發SIGINT信號,父進程接受信號後發送給子進程,若是子進程存在SIGINT偵聽函數,則會執行該函數,不然執行exit系統調用子進程退出。所以,若是要讓子進程在接收到SIGINT信號不退出,只須要不做處理便可:函數
file: c.js process.on('SIGINT',function(){ console.log('child sigint'); }); setTimeout(()=>{ console.log('children exit'); },10000)
以上實現,能夠知足咱們最初指定的目標:「父進程退出或者中斷,子進程仍正常運行」。gitlab
node官方提供了建立daemon進程的相關API,若是不仔細閱讀文檔還真不容易發現該特性。在child_process模塊中有個spawn函數,經過spawn能夠執行shell命令及其相關選項,同時spawn提供了建立子進程的一些選項,其中「detached」選項則與咱們的需求密切相關。
detached選項可讓node原生幫咱們建立一個daemon進程,設置datached爲true能夠建立一個新的session和進程組,子進程的pid爲新建立進程組的組pid,這與setsid起到相同的做用。此時的子進程已經和其父進程屬於兩個session,所以父進程的退出和中斷信號不會傳遞給子進程,子進程不會接受到父進程的中斷信號天然也不會退出。當父進程結束以後,子進程變爲孤兒進程從而被init進程接收,ppid設置爲1。
file: parent.js let cp = require('child_process'); const sp = cp.spawn('node',['./c.js'],{ detached: true, stdio: [process.stdin,process.stdout,process.stdout] }); sp.unref(); setTimeout(()=>{console.log('parent out')},5000); ---------------------- file: c.js setTimeout(()=>{ console.log('children exit'); },100000)
此時,c.js文件並未設置SIGINT事件偵聽函數,在父進程中斷後仍會正常運行,正是因爲其和父進程分屬於兩個session。
在parent.js文件中設置了sp.unref()
函數,目的是「避免父進程等待子進程退出」。那麼爲什麼會出現上述狀況呢?這與node的事件循環有關,讓父進程的事件循環排除對ChildProcess子進程對象的引用,可使父進程單獨退出。
爲何上文介紹的兩個方法均可以實現daemon進程呢?這還得回到系統層面進行分析。在linux系統建立一個daemon進程須要幾個步驟:
父進程建立子進程,父進程退出,讓子進程成爲孤兒進程,ppid=1
經過setsid命令或函數在子進程中建立新的會話和進程組
設置當前目錄
設置文件權限,並關閉父進程繼承打開的fd
所謂會話和進程組,則是在linux多任務多用戶下的概念。不一樣會話的進程沒法經過通訊,所以父子進程相隔離。而執行setsid命令則讓子進程有了新的特性:
子進程脫離父進程所在的session控制,二者獨立存在互不影響
子進程脫離父進程所在的進程組
子進程脫離原先的命令行終端,終端退出不影響子進程
下面再回顧方法一與方法二的區別,發現方法一其實並非真正的daemon進程,只是經過偵聽相關中斷信號並設置nop函數(不執行默認的中斷行爲)保證子進程繼續運行而已;而方法二則是標準的deamon進程建立方式,優先使用!