问题
今天发现服务器node进程出现了 好多僵尸进程:
root 229984 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 229991 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 229999 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 230005 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 230013 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 230020 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 230026 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 230034 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 230036 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 230047 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236001 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236008 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236015 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236022 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236037 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236044 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236051 1 0 Jun16 ? 12:03:37 [node] <defunct>
root 236058 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236065 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236072 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236079 1 0 Jun16 ? 00:00:00 [node] <defunct>
root 236086 1 0 Jun16 ? 00:00:00 [node] <defunct>
僵尸进程
<defunct>
是一个进程状态,通常也被称为僵尸进程(Zombie Process)。它表示一个子进程已经结束执行,但其父进程尚未将其完全清理,导致该子进程的进程描述符仍然存在于系统进程表中,但是该进程已经不能执行任何操作。
- 当一个子进程退出时,并不立刻清空进程表,而是向父进程发送一个信号。父进程需要对此应答,然后系统会完全清除子进程。假设父进程没有应答,或者应答之前子进程退出,子进程会被系统设置为‘僵尸’状态
- 在 Unix/Linux 系统中,进程终止后会产生一个进程描述符,称为“僵尸进程”,这个进程描述符中包含了进程的一些基本信息,例如进程 ID、状态等。如果进程的父进程没有及时清理僵尸进程,这些进程描述符就会一直存在于系统进程表中,占用系统资源,直到系统重启或者父进程结束。
通常情况下,僵尸进程不会对系统造成任何影响,但如果过多存在僵尸进程,会占用过多的系统资源,导致系统运行缓慢。为了避免出现这种情况,系统管理员需要及时清理僵尸进程,可以通过以下命令来查找和清理僵尸进程:
ps aux | grep '<defunct>'
从第三列可以看出这些进程的PPID 父进程都是1,都是系统进程。name这些进程就是孤儿进程
孤儿进程
孤儿进程(Orphan Process)是指父进程先于子进程结束,导致子进程成为没有父进程的进程。在 Unix/Linux 系统中,当一个进程结束时,内核会向其父进程发送一个信号,通知父进程该子进程已经结束。如果父进程没有及时处理该信号,或者父进程已经结束,子进程就会成为孤儿进程。
孤儿进程会占用系统资源,因为它们的进程描述符仍然存在于系统进程表中,但是它们已经没有可以执行的操作。为了避免出现孤儿进程,Unix/Linux 系统中通常会使用一些机制来确保子进程在父进程结束前被正确处理,例如:
- 使用
wait()
函数等待子进程结束,并处理子进程的退出状态。 - 将子进程的进程组 ID 设置为与父进程相同,这样当父进程结束时,所有与其进程组相同的子进程都会收到 SIGHUP 信号,从而结束执行。
- 将子进程的父进程设置为 init 进程(进程 ID 为 1),这样即使父进程结束后,子进程仍然有一个有效的父进程,并且 init 进程会定期清理孤儿进程。
- ‘孤儿进程’会立刻被‘init’超级进程接管,作为其父进程。‘init’进程能够确保这些子进程在退出时不会变为‘僵尸进程’,因为‘init’进程总是应答子进程的退出
变成孤儿进程后,也会占用系统资源
236149 root 20 0 624204 72484 28728 R 29.1 3.5 723:14.80 node
236051 root 20 0 617316 65956 28728 R 28.8 3.1 723:15.12 node
236100 root 20 0 617500 66352 28736 R 28.8 3.2 723:54.82 node
236128 root 20 0 620968 69120 28784 R 28.8 3.3 723:26.47 node
236092 root 20 0 616792 65756 28872 R 28.5 3.1 723:11.56 node
236134 root 20 0 623672 72012 28804 R 28.5 3.4 715:45.44 node
236121 root 20 0 617476 66336 28828 R 27.8 3.2 715:53.03 node
// c查看僵尸进程
ps aux | grep '<defunct>'
ps -o pid,ppid,command,stat | grep node
查看node 进程
ps aux | grep node
// 查看所有进程
ps -ef | grep node
解决方法
僵尸进程
当通过对子进程调用 subProcess.kill()
方法来终止进程时,有时候会出现子进程成为僵尸进程(defunct)的情况。这是由于父进程没有正常地处理子进程退出的信号(SIGCHLD),导致子进程的资源没有被完全回收。
要解决这个问题,可以在父进程中监听 SIGCHLD 信号,并在信号处理程序中调用 waitpid()
系统调用来回收子进程的资源。waitpid()
系统调用会挂起父进程,直到子进程结束,并返回子进程的状态信息,这样就可以避免子进程成为僵尸进程。
以下是一个使用 Node.js 中 child_process.spawn()
方法创建子进程的示例,演示了如何监听 SIGCHLD 信号并处理僵尸进程问题:
const { spawn } = require('child_process');
const subProcess = spawn('ls', ['-l']);
subProcess.on('exit', (code, signal) => {
console.log(`子进程退出:${code} ${signal}`);
});
// 监听 SIGCHLD 信号
process.on('SIGCHLD', () => {
console.log('收到 SIGCHLD 信号');
// 回收子进程资源
while (true) {
try {
const [pid, status] = await Promise.all([
new Promise((resolve) => {
const handler = (pid) => {
resolve(pid);
process.removeListener('exit', handler);
};
process.on('exit', handler);
}),
new Promise((resolve) => {
const handler = () => {
resolve();
process.removeListener('SIGCHLD', handler);
};
process.on('SIGCHLD', handler);
}),
]);
console.log(`回收子进程 ${pid} 资源,状态:${status}`);
} catch (error) {
break;
}
}
});
// 终止子进程
subProcess.kill();
在上面的示例中,我们使用 process.on('SIGCHLD', handler)
方法监听 SIGCHLD 信号,并在信号处理程序中使用 waitpid()
系统调用回收子进程的资源。这样,就可以避免子进程成为僵尸进程。
需要注意的是,如果父进程没有及时回收子进程的资源,可能会导致系统出现资源不足或系统崩溃等问题。因此,在编写程序时应该注意处理子进程退出的信号,并及时回收子进程的资源。
SIGCHLD
是 Linux 系统中的一个信号,用于通知父进程子进程已经退出。当一个子进程退出时,内核会向其父进程发送 SIGCHLD
信号,以便父进程可以知道子进程已经退出,并可以使用 wait()
系统调用来回收子进程的资源。
在 Linux 系统中,如果父进程没有及时处理 SIGCHLD
信号,子进程就会成为僵尸进程(defunct),导致系统资源浪费。因此,父进程应该及时处理 SIGCHLD
信号,并回收子进程的资源。
孤儿进程
如果在 Node.js 中,主进程退出时子进程还在运行,子进程会变成孤儿进程,而不是僵尸进程。孤儿进程是指其父进程已经退出或者已经被杀死,但是进程本身仍在运行的进程。孤儿进程将成为 init 进程的子进程,并由 init 进程接管其管理。它的父进程 ID(PPID)会被设置为 1,也就是 init 进程的进程 ID。
在 Node.js 中,如果子进程还在运行,但是主进程已经退出,可以使用 process.on('exit', callback)
事件来捕获主进程退出事件,并在回调函数中终止子进程。以下是一个示例:
const { spawn } = require('child_process');
const subProcess = spawn('node', ['child.js']);
process.on('exit', (code) => {
console.log(`主进程退出:${code}`);
subProcess.kill(); // 终止子进程
});
subProcess.on('exit', (code, signal) => {
console.log(`子进程退出:${code} ${signal}`);
});
在上面的示例中,我们使用 child_process.spawn()
方法创建子进程,并使用 process.on('exit', callback)
事件捕获主进程退出事件。在回调函数中,我们终止子进程,避免它成为孤儿进程。
需要注意的是,终止子进程可能需要一定的时间,因此在终止子进程之前,可能需要先等待一段时间,以确保子进程能够正常退出。可以使用 setTimeout()
函数来延迟终止子进程的时间,或者使用 childProcess.kill()
方法强制终止子进程。