在shell管道中,提升对孤儿进程和存在漏洞的应用程序的容错能力
进一步说明:不一致之处已被确认是由于工具界面变更,less 导致向后兼容性被破坏。从版本603开始,Beginning with version 603 需要提供选项 --exit-follow-on-close,以在未指定新选项的情况下维持与早前相同的行为。
注:该问题并非与 Getting ssh to execute a command in the background on target machine 的重复问题。另一个问题与可能被发送挂起信号的后台进程有关。
当前的问题与在前台进程结束后达到管道终止的位置相关,即 my_buggy_application,它处理管道内的输入/输出,同时也可能留下孤儿进程。
事实上,在这种情况下管道仍然保持打开状态,尽管使用了 nohup,甚至在前台进程终止之后也是如此。nohup 默认会将进程与标准输入输出断开,这也是在所提议的重复问题中被认为在当前场景下有效的唯一意义的原因所在。然而,这种断开在没有 nohup 的情况下也很容易实现,并且也破坏了所呈现场景的预期功能。
因此,nohup 与问题无关。
确实,问题的任何部分都没有涉及挂断信号。
所提议的重复并非真正的重复,而在当前情境下,所提议重复中的解决方案并无效。
开一个新的Bash会话,可以很容易地看到shell没有持久化的子进程…
% pstree $$
bash───pstree
此外,简单的测试会得到可预见的结果…
% ( echo hello; echo world ) | cat
hello
world
% pstree $$
bash───pstree
然而,一个稍微复杂的测试得到的结果就不那么明显…
( echo hello; sleep infinity & echo world ) | cat
hello
world
^C
输出并不令人惊讶,但不太明显的是,命令不会自动退出,而必须通过干预来终止。
同样,可以再考虑一个进一步的测试…
% ( echo hello; sleep infinity & echo world ) | cat &
hello
world
% pstree $$
bash─┬─cat
└─pstree
由于作为异步调用,该命令会立即返回到shell,但它仍然让子进程保持打开,可能需要干预来终止。
与此同时,sleep 命令持续存在,作为一个孤儿进程,即使它仍在系统进程表中,在本地进程树中却未显示。
尽管孤儿进程的出现并不令人意外,但普遍的倾向是,一旦直接进程退出,输入端就应关闭,即使仍有子进程持续存在。如果持续的进程不是子进程,而是一个孤儿后代,且直接子进程已经退出,这种情况就显得更有说服力。
为论证起见,若将 sleep infinity & 换成对某应用的调用,而该应用存在一个漏洞,导致并非所有后代进程都能正确退出,会怎样呢。
这种调用本身不一定是异步的。
例如考虑…
( echo hello; my_buggy_application; echo world ) | cat
在一般情况下,编写的shell脚本作者无法控制命令是否会留下持续存在的孤儿进程,但通常希望管道中的命令在差异存在时仍能具有可预测的行为。
实际上,孤儿进程不一定是无意的或不可取的。被调用的应用 my_buggy_application 在产生孤儿进程方面通常是健全的。
一个shell脚本如何防范因持续存在的子进程和孤儿进程而导致的不可预测结果呢?
解决方案
一种控制进程的方式是使用trap-and-wait的组合:
#!/bin/bash
function close {
pkill -9 -P $PID
exit
}
trap close SIGINT SIGABRT SIGTERM
echo hello
my_buggy_application &
PID=$!
wait $PID
echo world
close
通过这种方式,错误的应用被放到后台,以便在按下Ctrl+C时脚本可以将其终止。
最终的 close 本不需要,但也无伤大雅。