带你走近Node中的孤儿进程/僵尸进程/守护进程

1,435 阅读6分钟

孤儿进程

父进程已经退出,它的一个或者多个子进程还在运行,上述的子进程会成为孤儿进程

孤儿进程会被 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 已经变成了孤儿进程

image.png

僵尸进程

主进程通过 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) { } // 主进程永久阻塞
}

image.png

进程的状态

  • 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');
        });
}

僵尸进程和孤儿进程区别

image.png

守护进程

是什么

守护进程是一个在后台运行并且不受任何终端控制的进程,并且能够因为某个异常导致进程退出的时候重启子进程 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行

为什么脱离终端

当我们编写 Node 代码的时候,我们会使用终端来启动一个进程,通过命令行(Ctrl + C)或者手动关闭终端,当前程序会被关掉这样进程也被关掉了。 但是对于守护进程来说,它需要脱离这种限制,需要在整个系统关闭时才退出,所以不能够依赖终端

如何查看守护进程

使用命令 ps axj a: 显示所有 x: 显示没有终端控制的进程 j: 显示相关信息

image.png

进程组/会话/控制终端

进程组是什么

  • 每个进程属于一个进程组
  • 每个进程组都有一个进程组号(PGID),等于该进程组组长的PID
  • 一个进程只能为它自己或子进程设置 PGID 号,默认情况下新创建的进程会继承父进程的 PGID

会话期是什么

  • 是一个或多个进程组的集合,使用 SID 来标识
  • 登录后新建一个会话,登录之后的第一个进程就是会话领头进程,对于领头进程来说 PID = SID
  • 一个会话对应一个控制终端

控制终端

一个会话会有一个控制终端用于执行操作,控制进程打开一个终端之后,该终端就会成为当前会话的控制终端

前台进程组/后台进程组

前台进程组:该进程组的进程能够对终端设备进行读写操作 后台进程组:该进程组的进程能够对终端设备进行写操作

它们的关系

image.png

怎么做

实现监听子进程退出

利用 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 命令杀掉对应的子进程,能够成功重启子进程,守护进程生效~

image.png

如何退出终端运行?

使用setsid,那 setsid 能做什么?

  • 该进程变成一个新会话的会话组长
  • 该进程变成一个新进程组的组长
  • 该进程没有控制终端

在 Node 中如何调用 setsid 方法呢? Node 尚未对 setsid 进程封装,但是我们可以通过 child_process.spawn 来调用该方法。 spawn 中有一个配置项 detached,当其detached: true时,会调用 setsid 方法 > 在非 Windows 平台上,如果 options.detached 设置为 true,则子进程将成为新进程组和会话的领导者。 子进程可以在父进程退出后继续运行。

开始实现

  1. 在 command 中使用 child_process.spawn(master) 创建子进程
  2. 对进程 master 执行 setsid 方法
  3. 进程 command 退出,进程 master 由 init 进程接管,此时进程 master 为守护进程

创建command,使用 spawn 方法衍生子进程

//command.js
const { spawn } = require('child_process');

spawn('node', ['./master.js'], {
    detached: true,
});

process.exit(0);

image.png

当我们执行 node command 之后,我们的主进程就会关闭,但是我们的子进程还在继续运行,且不受终端控制,该进程就是我们创建的守护进程

🤔 我们为什么会 fork/spawn 两次? 第一次执行spawn:让 master 去调用 setsid 方法,成为会话期的组长,切不受终端的控制 第二次执行fork:因为第一次 spawn 出来的 master 通过 setsid 成为会话期的组长,能够再一次打开控制终端。再一次 fork 之后,创建出来 log 进程,无法打开新的控制终端