背景
在node工程部署中,常常涉及到三方:本地客戶端、跳板機和伺服器(叢集)。在通過git觸發gitlab hook指令碼後,需要在跳板機中執行相應的ssh命令執行shell檔案啟動node伺服器,這需要使用一個常用的命令setsid,這樣當ssh命令執行完畢shell退出後,node伺服器仍正常執行,此時node服務程式就是一個最典型的daemon程式(後臺服務程式)。
那麼,在node專案中,如何建立一個daemon程式呢?最簡單的方式,其實就是採用類似上文中介紹的方式:
1 |
require('child_process').exec('setsid node app.js >/dev/null 2>&1 &'); |
這樣可以通過執行shell的方式實現daemon程式。不過本文的重點並不是介紹這種“命令列”的方式實現daemon程式,而且本文會詳細講述daemon程式的建立原理,且看下文。
目標
在當前業務中,之所以需要建立daemon程式就是為了保證中斷建立該程式的父程式(ctrl+c)或者父程式執行完畢後並不影響daemon程式的執行。下文介紹兩種實現方式,實現原理細節上有些出入。
下文中的所有討論都是在linux環境下進行。
實現一
在linux系統中,父程式建立出子程式,此時父程式若退出,此時子程式則變為孤兒程式,其ppid變為1,即成為init程式的子程式。在node環境下,如果不針對子程式的stdio做一些特殊處理父程式其實不會真正退出,而是直到子程式執行完畢後再退出。之所以出現這種情況是由於node建立子程式時預設會通過pipe方式將子程式的輸出導流到父程式的stream中(childProcess.stdout、childProcess.stderr),提供在父程式中輸出子程式訊息的能力。
因此,解決此種問題可給子程式的stdio重新賦值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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後子程式退出。
上述實現只滿足“父程式正常退出,子程式成為守護程式”的情況,一旦通過“ctrl+c”的方式終端父程式,子程式仍會退出,這還是與node底層實現有關。預設“ctrl+c”觸發SIGINT訊號,父程式接受訊號後傳送給子程式,如果子程式存在SIGINT偵聽函式,則會執行該函式,否則執行exit系統呼叫子程式退出。因此,如果要讓子程式在接收到SIGINT訊號不退出,只需要不作處理即可:
1 2 3 4 5 6 7 8 9 |
file: c.js process.on('SIGINT',function(){ console.log('child sigint'); }); setTimeout(()=>{ console.log('children exit'); },10000) |
以上實現,可以滿足我們最初指定的目標:“父程式退出或者中斷,子程式仍正常執行”。
實現二
node官方提供了建立daemon程式的相關API,如果不仔細閱讀文件還真不容易發現該特性。在child_process模組中有個spawn函式,通過spawn可以執行shell命令及其相關選項,同時spawn提供了建立子程式的一些選項,其中“detached”選項則與我們的需求密切相關。
detached選項可以讓node原生幫我們建立一個daemon程式,設定datached為true可以建立一個新的session和程式組,子程式的pid為新建立程式組的組pid,這與setsid起到相同的作用。此時的子程式已經和其父程式屬於兩個session,因此父程式的退出和中斷訊號不會傳遞給子程式,子程式不會接受到父程式的中斷訊號自然也不會退出。當父程式結束之後,子程式變為孤兒程式從而被init程式接收,ppid設定為1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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程式建立方式,優先使用!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!