【问题标题】:Bash script kill background (grand)children on Ctrl+CBash 脚本在 Ctrl+C 上杀死背景(大)子级
【发布时间】:2023-03-18 05:15:01
【问题描述】:

我有一个 Bash 脚本(Bash 3.2、Mac OS X 10.8)并行调用多个 Python 脚本,以便更好地利用多个内核。每个 Python 脚本都需要很长时间才能完成。

问题是,如果我在 Bash 脚本中间按 Ctrl+C,Python 脚本实际上并没有被杀死。我如何编写 Bash 脚本以便杀死它也会杀死它的所有背景子项?

这是我原来的“简化测试用例”。不幸的是,我似乎已经减少了很多,以至于它不再证明问题了;我的错。

set -e

cat >work.py <<EOF
import sys, time
for i in range(10):
    time.sleep(1)
    print "Tick from", sys.argv[1]
EOF

function process {
    python ./work.py $1 &
}

process one
process two
wait

这是一个完整的测试用例,仍然高度简化,但希望这个可以证明问题。它在我的机器上重现......但是,两天前我认为 old 测试用例在我的机器上重现,今天它绝对没有。

#!/bin/bash -e
set -x

cat >work.sh <<EOF
for i in 0 1 2 3 4 5 6 7 8 9; do
    sleep 1; echo "still going"
done
EOF
chmod +x work.sh

function kill_all_jobs { jobs -p | xargs kill; }
trap kill_all_jobs SIGINT

function process {
    ./work.sh $1
}

process one &
wait $!
echo "All done!"

即使在 Ctrl+C 之后,此代码仍会继续打印 still going。但是,如果我将&amp; 从外部process 移动到内部(即:./work.sh $1 &amp;),那么 Ctrl+C 将按预期工作。我完全不明白!

在我的真实脚本中,process 包含多个命令,并且命令是长时间运行的,必须按顺序运行;所以在这种情况下,我不知道如何“将&amp; 移动到process 中”。我确信这是可能的,但它必须是不平凡的。

$ bash --version
GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin12)
Copyright (C) 2007 Free Software Foundation, Inc.

编辑:非常感谢@AlanCurry 教我一些 Bash 的东西。不幸的是,我仍然不明白我的示例中到底发生了什么,但这实际上是一个有争议的问题,正如 Alan also 有帮助地指出,对于我现实世界的并行化问题,Bash 是错误的工具,并且我应该使用带有make -j3 的简单makefile! make 尽可能并行运行,也完美理解 Ctrl+C;问题已解决(即使问题未得到解答)。

【问题讨论】:

  • trap 应该可以在 shell 函数中正常工作。我拿了你的脚本,在顶部添加了你的 killstuff 定义和 trap killstuff SIGINT 命令,用 bash 运行它,它运行良好。不过,我使用的是 Linux,而不是 MacOS,所以也许这就是你的行为不同的原因。尝试在顶部使用set -x 运行,它会生成一些调试输出,您可以在此处发布。
  • @AlanCurry:你说得对,我的旧测试用例实际上并没有重现这个问题。 :( 昨天我以为是这样,但我一定把它的几个不同版本混在一起了。正如我的新测试用例所示,即使是一个字符也会对脚本的行为产生很大影响。所以,想再试一次是吗?
  • 我还没有一个好的解决方案,但我可以解释发生了什么。当您在后台运行 shell 函数时,它会在子 shell(一个单独的进程)中运行。所以你有 3 个进程正在运行:运行脚本的主 shell、运行函数的子 shell 和 python 进程。您的 kill_all_jobs 函数将 SIGTERM 发送到子shell,杀死它但不杀死孙子 python 进程。所有这一切都是必要的,因为python的默认 SIGINT 处理程序拒绝在按下原始 ^C 时死亡。 (python 在前台进程组中,所以它确实得到一个 SIGINT)
  • 但是我的新测试用例根本不使用 Python,所以这不可能是全部故事......?不过,这肯定与无法杀死孙子进程有关。
  • 进一步的研究表明,主要问题是 SIGINT 通常被后台进程忽略。在主脚本退出后让他们继续运行是一个有意的功能,而且很难解决。您甚至不能将trap - 2 放在 work.sh 中,因为内置的 trap 拒绝取消忽略脚本启动时被忽略的信号。

标签: bash shell process wait kill


【解决方案1】:

我明白了!你所要做的就是摆脱那个 python SIGINT 处理程序。

cat >work.py <<'EOF'
import sys, time, signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
for i in range(10):
    time.sleep(1)
    print "Tick from", sys.argv[1]
EOF 
chmod +x work.py

function process {
    python ./work.py $1
}

process one &
wait $!
echo "All done!"

【讨论】:

    【解决方案2】:

    你的trap 我觉得不错:

    $ bash --version
    GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin11)
    Copyright (C) 2007 Free Software Foundation, Inc.
    
    $ cat ./thang 
    #! /bin/bash
    set -e
    
    cat >work.py <<EOF
    import sys, time
    for i in range(10):
      time.sleep(1)
      print "Tick from", sys.argv[1]
    EOF
    
    function process {
      python ./work.py $1 &
    }
    
    function killstuff {
      jobs -p | xargs kill
    }
    
    trap killstuff SIGINT
    
    process one
    process two
    wait
    
    $ ./thang 
    Tick from one
    Tick from two
    Tick from one
    Tick from two
    ^C$ ps aux | grep python | grep -v grep
    $
    

    【讨论】:

    • 我以某种方式搞砸了测试用例。问题一定不在于trap,而与哪些进程完全算作我的孩子、我的孙子或其他人的孩子有关。看看新的测试用例?顺便说一句,感谢jobs -p | xargs kill 的成语;这比我原来的 for 循环好多了。
    猜你喜欢
    • 2013-05-20
    • 2021-12-15
    • 2016-01-24
    • 2021-02-23
    • 2019-07-13
    • 2017-12-05
    • 1970-01-01
    • 1970-01-01
    • 2017-03-31
    相关资源
    最近更新 更多