杀死由Bash脚本启动的所有进程

3
我有许多bash脚本正在执行许多相似的任务,它们使用一些外部二进制程序。问题在于这些二进制程序经常无法正常退出终止。由于我的脚本要执行它们数千次,这些闲置或几乎死亡的进程实例会迅速累积。我无法修复这些程序,因此需要确保我的bash脚本终止它们。
已经有一些涉及终止bash脚本进程的话题在SE中。我已经应用和测试了那些内容,而且在某种程度上它们确实可行。但是对于我的情况来说并不够好,我不明白为什么,因此我开了一个新的问题。
我的脚本具有层次结构,以下是简化后的示例: 脚本A调用脚本B,脚本B并行调用多个脚本C实例以使用所有CPU。例如,脚本B并行运行5个脚本C实例,并且当其中一个脚本C实例完成时,它将启动一个新的实例,总共运行数千次脚本C。脚本C调用多个外部二进制程序/命令,它们没有很好地终止。它们在后台并行运行并相互通信。
然而,我的脚本C能够检测到外部命令何时完成它们的工作,即使它们没有终止,然后我的bash脚本退出。
为了在bash脚本完成时终止所有外部程序,我添加了一个退出陷阱:
# Exit cleanup
cleanup_exit() {
    # Running the termination in an own process group to prevent it from preliminary termination. Since it will run in the background it will not cause any delays
    setsid nohup bash -c "
        touch /tmp/trace_1  # To see if this code was really executed to this point

        # Trapping signals to prevent that this function is terminated preliminary
        trap '' SIGINT SIGQUIT SIGTERM SIGHUP ERR
        touch /tmp/trace_2  # To see if this code was really executed to this point

        # Terminating the main processes
        kill ${pids[@]} 1>/dev/null 2>&1 || true
        touch /tmp/trace_3
        sleep 5
        touch /tmp/trace_4
        kill -9 ${pids[@]} 1>/dev/null 2>&1 || true
        touch /tmp/trace_5

        # Terminating the child processes of the main processes
        echo "Terminating the child processes"
        pkill -P ${pids[@]} 1>/dev/null 2>&1 || true
        touch /tmp/trace_6
        sleep 1
        pkill -9 -P ${pids[@]} 1>/dev/null 2>&1 || true
        touch /tmp/trace_7

        # Terminating everything else which is still running and which was started by this script
        pkill -P $$ || true
        touch /tmp/trace_8
        sleep 1
        pkill -9 -P $$ || true
        touch /tmp/trace_9
    "
}
trap "cleanup_exit" SIGINT SIGQUIT SIGTERM EXIT

现在,如果我只同时运行很少数量的C脚本实例,似乎这个方法可以工作。但是,如果我增加数量到更多,例如10个(工作站很强大,应该能够处理数十个并行的C脚本实例和外部程序),那么它就不能再工作了,并且数百个外部程序实例会迅速累积。
但我不明白为什么。例如,其中一个积累的进程PID是32048。在日志中,我可以看到执行退出陷阱:
+ echo ' * Snapshot 190 completed after 3 seconds.'
 * Snapshot 190 completed after 3 seconds.
+ break
+ cleanup_exit
+ echo

+ echo ' * Cleaning up...'
 * Cleaning up...
+ setsid nohup bash -c '
        touch /tmp/trace_1  # To see if this code was really executed to this point

        # Trapping signals to prevent that this function is terminated preliminary
        trap '\'''\'' SIGINT SIGQUIT SIGTERM SIGHUP ERR
        touch /tmp/trace_2  # To see if this code was really executed to this point

        # Terminating the main processes
        kill 31678' '32048 1>/dev/null 2>&1 || true
        touch /tmp/trace_3
        sleep 5
        touch /tmp/trace_4
        kill -9 31678' '32048 1>/dev/null 2>&1 || true
        touch /tmp/trace_5

        # Terminating the child processes of the main processes
        pkill -P 31678' '32048 1>/dev/null 2>&1 || true
        touch /tmp/trace_6
        sleep 1
        pkill -9 -P 31678' '32048 1>/dev/null 2>&1 || true
        touch /tmp/trace_7

        # Terminating everything else which is still running and which was started by this script
        pkill -P 31623 || true
        touch /tmp/trace_8
        sleep 1
        pkill -9 -P 31623 || true
        touch /tmp/trace_9
    '

显然,该进程的PID被用于退出陷阱,但是进程没有退出。为了测试,我手动再次对这个进程运行了kill命令,然后它确实退出了。

最有趣的是,只有前5个跟踪文件出现了。为什么没有超过5个呢?

更新:我刚刚发现即使我并行运行脚本C的一个实例,即顺序运行,它也只能在一段时间内正常工作。突然在某个时间点,进程不再被终止,而是开始永久地挂在那里并积累。机器不应该被一个并行进程过载。在我的日志文件中,退出陷阱仍然像以前一样被正确调用,没有任何区别。内存也是空闲的,CPU也部分空闲。


1
请阅读 https://stackoverflow.com/help/mcve 并尝试创建最小化、完整化和可验证的示例。 - Chen A.
@Vinny:一般来说,这是一个好主意,但在这种情况下会非常困难,因为我的脚本使用的外部程序是大型复杂的科学软件包(不在任何仓库中),而且我没有其他更简单的程序可以重现它们的行为。 - Jadzia
嗯,想象一下,有人想尝试并提供帮助,需要大约10分钟来阅读问题。只是说,如果问题更短(或更针对问题所在),你的问题会更具响应性。 - Chen A.
@Vinny:我理解你的观点。我试图只包含相关信息,但上下文对于解决方案也很重要,因为它排除了某些解决方案。如果我进一步缩小问题范围并省略一些上下文信息,可能会与已经提出的其他问题重合。因此,我想保留一些特定情况的细节。 - Jadzia
1个回答

5

对于任何shell脚本来说,一个好的健康检查是在其上运行ShellCheck:

Line 9:
        kill ${pids[@]} 1>/dev/null 2>&1 || true
             ^-- SC2145: Argument mixes string and array. Use * or separate argument.

实际上,这行代码中您的xtrace表现得有些奇怪:

kill 31678' '32048 1>/dev/null 2>&1 || true
          ^^^--- What is this?

这里的问题是你的${pids[@]}展开成了多个单词,bash -c只会解释第一个单词。以下是一个简化的例子:
pids=(2 3 4)
bash -c "echo killing ${pids[@]}" 

这最终会输出killing 2,没有提及3或4。这等同于运行:

bash -c "echo killing 2" "3" "4" 

其他pid只是变成了位置参数$0$1,而不是被包含在执行命令中。

相反,像ShellCheck建议的那样,您希望使用*将所有pid与空格连接起来,并将它们作为单个参数插入:

pids=(2 3 4)
bash -c "echo killing ${pids[*]}" 

这段代码会输出:killing 2 3 4


你真是太聪明了,非常感谢你的帮助!我现在就要测试一下... :-) - Jadzia
首次测试似乎确实有效,到目前为止没有积累任何进程。然而,为了得出最终结论,我需要运行更长时间的测试。同时我发现了其他问题。我在退出陷阱的每个步骤中添加了跟踪文件(请参见上面更新的帖子)。在添加您建议的更正之前,只有第一个跟踪文件出现。现在,通过您的更正,跟踪文件一直到第5个出现。但是没有更多了。有什么想法吗?脚本B在8秒后对脚本C调用kill -9,但是终止代码不应受影响,因为它是使用setsid运行的。 - Jadzia
实际上,即使使用setsid,pkill -P $$也可能会杀死退出代码,因为它的ppid是脚本C启动时的pid。但这意味着至少应该到达跟踪文件7。 - Jadzia
我在解决方案中找到了答案。我的原始代码中有一个echo命令,在trace-file 5后使用了双引号,bash似乎将其解释为退出代码的结尾... 再次感谢您的所有帮助。 - Jadzia
比起完全拆开并重新开始,这种方法要好得多!我已经删除了我的评论并点赞了这个答案。祝大家好运! - shellter
@shellter:仍然非常感谢您的建议,真的非常感激!(我现在已经删除了回应您评论的评论) - Jadzia

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