Node子进程:如何拦截像SIGINT这样的信号

18

在我的Node应用程序中,我挂接了SIGINT信号以便优雅地停止(使用pm2,但这里不相关)。

我的应用程序还会执行/生成一些子进程。

我能够挂接SIGINT以拦截它并执行优雅的停止,但是我的子进程通过相同的信号传递,因此会立即被杀死。

如何在我的子进程上拦截SIGINT信号?

以下是我正在做的示例:

const child = child_process.spawn('sleep', ['10000000']);
console.log(`Child pid: ${child.pid}`);

child.on('exit', (code, signal) => { console.log('Exit', code, signal); });

process.on('SIGINT', () => {
    console.log("Intercepting SIGINT");
});
3个回答

25

默认情况下,由child_process.spawn()创建的子进程与父进程具有相同的进程组,除非使用了{detached:true}选项进行调用。

要点是,该脚本在不同的环境中会有不同的行为:

// spawn-test.js
const { spawn } = require('child_process');
const one = spawn('sleep', ['101']);
const two = spawn('sleep', ['102'], {detached: true});
two.unref();
process.on('SIGINT', function () {
  console.log('just ignore SIGINT');
});

在交互式 shell 中,默认情况下,从 Ctl-C 发送的 SIGINT 信号将发送到整个进程组,因此非分离子进程将收到 SIGINT 信号并退出:

you@bash $ node spawn-test.js
^Cjust ignore SIGINT
# the parent process continues here, let's check children in another window:
you@bash [another-terminal-window] $ ps aux | grep sleep
... sleep 102
# note that sleep 101 is not running anymore
# because it recieved the SIGINT from the Ctl-C on parent

...但是对 kill(2) 的调用可以仅向您的父进程发出信号,因此子进程保持存活:

you@bash $ node spawn-test.js & echo $?
[2] 1234
you@bash [another-terminal-window] $ kill -SIGINT 1234
you@bash [another-terminal-window] $ ps aux | grep sleep
... sleep 101
... sleep 102
# both are still running

然而,pm2是另一个完全不同的东西。即使您尝试上述技术,它也会杀死整个进程树,包括您的分离进程,即使有一个很长的 --kill-timeout 参数:

# Test pm2 stop
you@bash $ pm2 start spawn-test.js --kill-timeout 3600
you@bash $ pm2 stop spawn-test
you@bash $ ps aux | grep sleep
# both are dead

# Test pm3 reload
you@bash $ pm2 start spawn-test.js --kill-timeout 3600
you@bash $ pm2 reload spawn-test
you@bash $ ps aux | grep sleep
# both have different PIDs and were therefore killed and restarted

这似乎是 pm2 中的一个 bug。

我曾经遇到类似的问题,通过使用 init 系统 (在我的情况下是 systemd) 而不是 pm2,可以更好地控制信号处理。

在 systemd 上,默认会向整个进程组发送信号,但您可以使用 KillMode=mixed 仅将信号发送给父进程,但如果子进程超时运行,则仍然 SIGKILL 子进程。

我的 systemd unit 文件如下:

[Unit]
Description=node server with long-running children example

[Service]
Type=simple
Restart=always
RestartSec=30
TimeoutStopSec=3600
KillMode=mixed
ExecStart=/usr/local/bin/node /path/to/your/server.js

[Install]
WantedBy=multi-user.target

感谢您的出色回答! - julien_c
1
不错的回答,尽管对于读者来说可能会有些困惑,因为你的父进程信号处理程序没有做任何事情来忽略信号并允许程序退出,但发出了一个字符串,表明你可能打算忽略该信号。 - Steven Lu
@StevenLu,我想我明白你在这里说的意思。处理信号可以防止默认行为(程序退出),因此在按下Ct1-C后,程序实际上仍在运行,但是我的控制台示例可能暗示它已经退出了。请注意,下一行有一个关于bash提示符的注释,说明它在另一个终端窗口中。我认为这不够清楚。我在正确的轨道上吗?也许我可以将示例分成两个代码块,以使其更清晰。 - onecreativenerd
@StevenLu,我刚编辑了第一个 bash 代码块,请告诉我现在是否更清楚了。 - onecreativenerd

5
通常在C语言中,您可以通过在子进程中忽略信号(或将其生成在新的进程组中,以便终端生成的前台进程组信号不会到达它)来解决这个问题。
https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options中看, NodeJs似乎没有提供此API,但是它确实有一个选项,可以通过shell生成子进程,所以您可以打开该选项并在shell中忽略信号,这将导致其被继承到shell的子级。
const child_process = require('child_process')
//const child = child_process.spawn('sleep', ['10000000']);
const child = child_process.spawn("trap '' INT; sleep 10000000", [], {shell: true });
console.log(`Child pid: ${child.pid}`);

child.on('exit', (code, signal) => { console.log('Exit', code, signal); });

process.on('SIGINT', () => {
    console.log("Intercepting SIGINT");
});


//emulate cat to keep the process alive
process.stdin.pipe(process.stdout);

现在,当你按下Ctrl-C时,Node进程会处理它,而睡眠进程会继续运行。 (如果您不熟悉其他终端生成的信号,您可以通过按Ctrl- \(发送SIGQUIT到该组)轻松杀死此组,如果您不介意核心转储的话)。


0

在pm2中有一个选项可以禁用杀死所有树进程,即使使用一个分离的子进程,它没有被很好地文档化,但如果你运行pm2 --help,你会注意到

--no-treekill  Only kill the main process, not detached children

或者如果您正在使用 ecosystem.config.js

treekill: false,

请查看https://github.com/Unitech/pm2/blob/master/lib/API/schema.json#L314, 该选项于2015年添加https://github.com/Unitech/pm2/pull/1395

希望这些信息对其他遇到相同情况的人有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接