节点子进程:如何拦截像SIGINT这样的信号
Node child processes: how to intercept signals like SIGINT
在我的 Node 应用程序中,我正在连接 SIGINT
信号以便正常停止(使用 pm2
,但这与此处无关)。
我的应用程序还有 execs/spawns 几个子进程。
我能够钩住 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");
});
通常在 C 中,您可以通过忽略 child 中的信号来解决这个问题(或者通过在新的进程组中生成它,以便终端为前台进程组生成的信号不会到达它)。
从 https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options 来看,NodeJs 似乎没有为此公开 API,但是,它确实有一个选项可以通过 shell,所以你可以做的是打开它并忽略shell中的信号,这将导致它的忽略状态被继承到shell的children .
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 进程会处理它并且睡眠进程会继续。
(如果您不熟悉其他终端生成的信号,如果您不介意 coredump,则可以通过按 Ctrl-\(向该组发送 SIGQUIT)轻松终止该组)。
默认情况下,child_process.spawn()
创建的 child 个进程具有相同的 process group as the parent, unless they were called with the {detached:true}
option。
结果是这个脚本在不同的环境中会有不同的表现:
// 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 默认情况下会发送到整个组,因此 non-detached child 将获得 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)
只能向您的 parent 进程发出信号,所以 children 还活着:
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 中的一个错误。
我通过使用 init 系统(在我的例子中是 systemd)而不是 pm2 解决了类似的问题,因为这样可以更好地控制信号处理。
在 systemd 上,信号默认发送到整个组,但您可以使用 KillMode=mixed
将信号仅发送到 parent 进程,但仍然 SIGKILL child如果它们 运行 超过超时则进行处理。
我的 systemd 单元文件如下所示:
[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
在我的 Node 应用程序中,我正在连接 SIGINT
信号以便正常停止(使用 pm2
,但这与此处无关)。
我的应用程序还有 execs/spawns 几个子进程。
我能够钩住 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");
});
通常在 C 中,您可以通过忽略 child 中的信号来解决这个问题(或者通过在新的进程组中生成它,以便终端为前台进程组生成的信号不会到达它)。
从 https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options 来看,NodeJs 似乎没有为此公开 API,但是,它确实有一个选项可以通过 shell,所以你可以做的是打开它并忽略shell中的信号,这将导致它的忽略状态被继承到shell的children .
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 进程会处理它并且睡眠进程会继续。 (如果您不熟悉其他终端生成的信号,如果您不介意 coredump,则可以通过按 Ctrl-\(向该组发送 SIGQUIT)轻松终止该组)。
默认情况下,child_process.spawn()
创建的 child 个进程具有相同的 process group as the parent, unless they were called with the {detached:true}
option。
结果是这个脚本在不同的环境中会有不同的表现:
// 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 默认情况下会发送到整个组,因此 non-detached child 将获得 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)
只能向您的 parent 进程发出信号,所以 children 还活着:
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 中的一个错误。
我通过使用 init 系统(在我的例子中是 systemd)而不是 pm2 解决了类似的问题,因为这样可以更好地控制信号处理。
在 systemd 上,信号默认发送到整个组,但您可以使用 KillMode=mixed
将信号仅发送到 parent 进程,但仍然 SIGKILL child如果它们 运行 超过超时则进行处理。
我的 systemd 单元文件如下所示:
[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