守护进程与Nodejs

守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。 ——百度百科

守护进程

概念

守护进程是在后台运行不受终端控制的进程(如输入、输出等),一般的网络服务都是以守护进程的方式运行。

  • 守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或循环等待处理某些事件的发生;
  • 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机才随之一起停止运行;
  • 守护进程一般都以root用户权限运行,因为要使用某些特殊的端口或者资源;
  • 守护进程的父进程一般都是init进程,因为它真正的父进程在fork出守护进程后就直接退出了,所以守护进程都是孤儿进程,由init接管;

场景:后台的一些系统服务进程,没有控制终端,不能直接和用户交互,不受用户登录、注销的影响,一直在运行着,他们都是守护进程,如:预读入缓输出机制的实现;ftp服务器(借助vsftpd实现),nfs服务器。

Linux 守护进程

Linux 服务器在启动时需要启动很多系统服务,它们向本地或网络用户提供了 Linux 的系统功能接口,直接面向应用程序和用户,而提供这些服务的程序就是由运行在后台的守护进程来执行的。

守护进程是生存期很长的一种进程,它们独立于控制终端,并且周期性地执行某种任务或等待处理某些发生的事件,多数的守护进程都伴随着 Linux 系统启动而启动,关闭而关闭。

Linux 系统中有很多守护进程,大多数服务器也都是用守护进程来实现的。不仅如此,某些守护进程还协助完成了很多系统任务,其中就包括负责计划任务的 atd 和 crond 服务。

Linux 系统中,选择运行哪些守护进程,要根据具体需求来决定,通过以 root 身份执行 ntsysv 命令,可以查看当前系统中拥有哪些守护进程,或者说能够提供哪些服务。

并且,按照启动和管理方式的不同,守护进程又可细分为 stand alone 和 xinetd 两类,在此不详细介绍。

创建

  • 1)fork()创建子进程,父进程 exit() 退出

这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

  • 2)在子进程中调用 setsid() 函数创建新的会话

在调用了fork()函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。

  • 3)再次 fork() 一个孙进程并让子进程退出

为什么要再次fork呢,假定有这样一种情况,之前的父进程fork出子进程以后还有别的事情要做,在做事情的过程中因为某种原因阻塞了,而此时的子进程因为某些非正常原因要退出的话,就会形成僵尸进程,所以由子进程fork出一个孙进程以后立即退出,孙进程作为守护进程会被init接管,此时无论父进程想做什么都随它了。

  • 4)在孙进程中调用 chdir() 函数,让根目录 ”/” 成为孙进程的工作目录

这一步也是必要的步骤,使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让”/“作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp,改变工作目录的常见函数是chdir。

  • 5)在孙进程中调用 umask() 函数,设置进程的文件权限掩码为0

文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

  • 6)在孙进程中关闭任何不需要的文件描述符

同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

在上面的第2)步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。

  • 7)守护进程退出处理

当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。

与Node服务

场景

  • 1.由于业务代码未考虑到导致的服务意外终止
  • 2.服务器重启或服务进程意外

这时候我们就需要自动重启了。

监听与重启

假如有个服务,代码文件为server.js。如果我们想监听这个代码文件,当代码文件修改时重启服务。


1
2
3
4
5
6
// server.js
const http = require('http');

http.createServer((req, res) => {
res.end('test');
}).listen(8000);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// listen.js
const fs = require('fs');
const exec = require('child_process').exec;

function watch () {
const child = exec('node server.js');
let watcher = fs.watch('./server.js', (event) => {
child.kill();
watcher.close();
watch();
});
}

watch();

方法

1.forever

npm-forever

1
forver start  bin/www

2.pm2

npm-pm2

1
pm2 strat bin/www

3.node自身进程保护

1
nohup node /bin/www  > 1.log &

平时在系统终端中执行一个命令后如果想立即的停止它,您可以同时按下系统组合键”Ctrl+c”,这样命令的进程将会立即被终止,是生产工作中比较常用的命令行快捷键之一。或者有些命令在执行时会不断的在屏幕上输出信息,影响到咱们继续输入命令了,便可以在执行命令时在命令最后面添加上一个”&”符号,这样命令从开始执行就默认被放到系统后台了。

4.node写自身进程保护

第一种方式:监听exit事件

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const fork = require('child_process').fork;

// 保存子进程实例
let workers = [];
let appsPath = ['./test.js', './app.js'];

const createWorker = function(appPath){
  let worker = fork(appPath);

  worker.on('exit',function(){
    console.log('worker:' + worker.pid + 'exited');
    delete workers[worker.pid];
    createWorker(appPath);
  });

  workers[worker.pid] = worker;

  console.log('Create worker:' + worker.pid);
};

// 启动所有子进程
for (let i = appsPath.length - 1; i >= 0; i--) {
  createWorker(appsPath[i]);
}

// 父进程退出时杀死所有子进程
process.on('exit',function(){
  for (let pid in workers){
    workers[pid].kill();
  }
});

第二种方式:定时通信

守护进程每10s与服务主进程进行一次通信,万一没有回应就重启它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* server.js 守护进程,用于检测服务工作是否正常,如果出现异常则进行重启
* 可以用forever/pm2启动,防止本进程异常退出,达到双重守护
*/

process.env.UV_THREADPOOL_SIZE = 128;
const exec = require('child_process').exec;

// 启动
function start () {
exec('pm2 start server.js', (err, stdout, stderr) => {
if (err) {
console.error(err);
return false;
}
console.log(`stdout: ${stdout}`);
console.log(`stdout: ${stderr}`);
})
}

// 停止
function stop () {
exec('pm2 stop server.js', (err, stdout, stderr) => {
if (err) {
console.error(err);
return false;
}
console.log(`stdout: ${stdout}`);
console.log(`stdout: ${stderr}`);
})
}

// 启动
start();

const request = request('request');
const HOST = 'http://127.0.0.1:3000';
setInterval(function () {
request.get(HOST, {
timeout: 5000
}, function (err) {
if (err) {
if (err.code === 'ETIMEDOUT' || err.code === 'ECONNREFUSED' || err.code === 'ESOCKETTIMEDOUT') { // 重启
stop();
start();
}
}
})
});

相关链接