在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进程建立方式,优先使用!