孤儿进程
父进程已经退出,它的一个或者多个子进程还在运行,上述的子进程会成为孤儿进程
孤儿进程会被 init 进程(pid为1)收养,并由 init 进程完成对它们的状态收集工作
const fork = require("child_process").fork
const server = require("net").createServer()
server.listen(3000)
const worker = fork("./worker.js")
process.title = "FBB master"
worker.send("server", server)
console.log(`worker created, ppid is ${process.pid}, pid is ${worker.pid}`)
const http = require("http")
const server = http.createServer((req, res) => {
res.end(`I am worker process, pid: ${process.pid}, ppid: ${process.ppid}`)
})
let worker = null;
process.on("message", (message, sendHandle) => {
if (message === "server") {
worker = sendHandle
worker.on("connection", (socket) => {
server.emit("connection", socket)
})
}
})
执行 node master.js,开启两个进程,当我们执行第一次curl http://127.0.0.1:3000,能够打印出来期望的 pid/ppid;当我们在活动监视器退出掉我们的 master 进程,再次执行,这个时候 ppid 就变成了1,此时的 worker 已经变成了孤儿进程
僵尸进程
主进程通过 fork 创建了子进程,如果子进程退出之后父进程没有获取子进程的状态信息,那么子进程中保存的进程号/退出状态/运行时间等都不会被释放,进程号会一直被占用
const fork = require("child_process").fork
zombie();
function zombie() {
const worker = fork("./worker.js");
console.log(`Worker is created, pid: ${worker.pid}, ppid: ${process.pid}`)
while (1) { } // 主进程永久阻塞
}
进程的状态
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 interruptible sleep)
- D磁盘休眠状态(Disk sleep): 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行
- X死亡状态(dead): 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
- Z僵死状态(zombie)
原本在 Node 中,子进程退出之后,父进程可以感知到并且清理子进程资源,正常情况下,开发者是无需感知的。
该示例之所以能够成为僵尸进程是因为while(1){}吃满了父进程的 CPU,无法处理子进程的退出信号。
修改上述代码,子进程退出之后,父进程可以监听到,就不会有僵尸进程的产生。
const fork = require("child_process").fork
zombie();
function zombie() {
const worker = fork("./worker.js");
worker
.on('exit', () => {
console.log('exit');
})
.on('close', () => {
console.log('close');
});
}
僵尸进程和孤儿进程区别
守护进程
是什么
守护进程是一个在后台运行并且不受任何终端控制的进程,并且能够因为某个异常导致进程退出的时候重启子进程 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行
为什么脱离终端
当我们编写 Node 代码的时候,我们会使用终端来启动一个进程,通过命令行(Ctrl + C)或者手动关闭终端,当前程序会被关掉这样进程也被关掉了。 但是对于守护进程来说,它需要脱离这种限制,需要在整个系统关闭时才退出,所以不能够依赖终端
如何查看守护进程
使用命令 ps axj a: 显示所有 x: 显示没有终端控制的进程 j: 显示相关信息
进程组/会话/控制终端
进程组是什么
- 每个进程属于一个进程组
- 每个进程组都有一个进程组号(PGID),等于该进程组组长的PID
- 一个进程只能为它自己或子进程设置 PGID 号,默认情况下新创建的进程会继承父进程的 PGID
会话期是什么
- 是一个或多个进程组的集合,使用 SID 来标识
- 登录后新建一个会话,登录之后的第一个进程就是会话领头进程,对于领头进程来说 PID = SID
- 一个会话对应一个控制终端
控制终端
一个会话会有一个控制终端用于执行操作,控制进程打开一个终端之后,该终端就会成为当前会话的控制终端
前台进程组/后台进程组
前台进程组:该进程组的进程能够对终端设备进行读写操作 后台进程组:该进程组的进程能够对终端设备进行写操作
它们的关系
怎么做
实现监听子进程退出
利用 NodeJS 的事件机制,监听 exit 事件
- 在 master.js 中使用 fork 创建子进程
- 监听 exit 事件,1s之后创建一个新的子进程
// master.js
const { fork } = require("child_process");
const forkChild = () => {
const child = fork('log.js')
child.on('exit', () => {
setTimeout(() => {
forkChild();
}, 1000);
});
}
forkChild();
// log.js
const fs = require('fs');
const path = require("path")
fs.open(path.resolve(__dirname, 'log.txt'), 'w', function (err, fd) {
setInterval(() => {
fs.write(fd, process.pid + "\n", function () { });
}, 2000)
});
使用 node master 启动项目之后,使用 kill 命令杀掉对应的子进程,能够成功重启子进程,守护进程生效~
如何退出终端运行?
使用setsid,那 setsid 能做什么?
- 该进程变成一个新会话的会话组长
- 该进程变成一个新进程组的组长
- 该进程没有控制终端
在 Node 中如何调用 setsid 方法呢?
Node 尚未对 setsid 进程封装,但是我们可以通过 child_process.spawn 来调用该方法。
spawn 中有一个配置项 detached,当其detached: true时,会调用 setsid 方法
> 在非 Windows 平台上,如果 options.detached 设置为 true,则子进程将成为新进程组和会话的领导者。 子进程可以在父进程退出后继续运行。
开始实现
- 在 command 中使用 child_process.spawn(master) 创建子进程
- 对进程 master 执行 setsid 方法
- 进程 command 退出,进程 master 由 init 进程接管,此时进程 master 为守护进程
创建command,使用 spawn 方法衍生子进程
//command.js
const { spawn } = require('child_process');
spawn('node', ['./master.js'], {
detached: true,
});
process.exit(0);
当我们执行 node command 之后,我们的主进程就会关闭,但是我们的子进程还在继续运行,且不受终端控制,该进程就是我们创建的守护进程
🤔 我们为什么会 fork/spawn 两次? 第一次执行spawn:让 master 去调用 setsid 方法,成为会话期的组长,切不受终端的控制 第二次执行fork:因为第一次 spawn 出来的 master 通过 setsid 成为会话期的组长,能够再一次打开控制终端。再一次 fork 之后,创建出来 log 进程,无法打开新的控制终端