在Bash中给定超时后如何杀死子进程?


178

我有一个bash脚本,该脚本会启动一个子进程,该子进程有时会崩溃(实际上是挂起),并且没有明显的原因(封闭源代码,因此我无能为力)。结果,我希望能够在给定的时间内启动此过程,如果在给定的时间之后未成功返回,则将其终止。

有没有简单强大的方法可以使用bash来实现这一目标?

PS:请告诉我这个问题是否更适合serverfault或超级用户。



Answers:


260

(如下所示: BASH常见问题解答条目68:“如何运行命令,并在N秒后使其中止(超时)?”

如果您不介意下载内容,请使用timeoutsudo apt-get install timeout)并按以下方式使用它:(大多数系统已经安装了它,否则请使用sudo apt-get install coreutils

timeout 10 ping www.goooooogle.com

如果您不想下载某些内容,请执行内部超时操作:

( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )

如果您想为更长的bash代码设置超时,请使用第二个选项,例如:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )

8
如果其他人想知道我做了什么,请Re Ignacio的答复:cmdpid=$BASHPID不会接受调用 shell 的pid,而是由开始的(第一个)子shell ()。该(sleep...事情调用第一子shell中的第二子shell等待10秒的背景和杀死第一子shell其中,已经推出了杀手子shell进程后,继续执行其工作量...
jamadagni

17
timeout是GNU coreutils的一部分,因此应该已经安装在所有GNU系统中。
Sameer

1
@Sameer:只为版本8
伊格纳西奥巴斯克斯-艾布拉姆斯

3
我不是100%确信这一点,但是据我所知(并且我知道我的手册告诉我的内容)timeout现在已经成为coreutils的一部分。
benaryorg

5
该命令不会“提前完成”。它总是会在超时时终止进程-但不会处理没有超时的情况。
鹰眼

28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

或同时获取退出代码:

# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?

8
kill -9在尝试发出可以先处理的信号之前,请勿使用。
暂停,直到另行通知。

没错,我只是想快速修复,只是假设他希望该过程立即终止,因为他说它崩溃了
Dan

8
这实际上是一个非常糟糕的解决方案。如果dosmth在2秒内终止,另一进程使用旧的pid,而您杀死了新的pid,该怎么办?
传送山羊

PID回收通过达到极限并环绕而起作用。除非系统完全处于混乱状态,否则其他进程不太可能在剩余的8秒内重用PID。
kittydoor '19

13
sleep 999&
t=$!
sleep 10
kill $t

这会导致过多的等待。如果实际命令(sleep 999此处)的完成时间通常比强制睡眠(sleep 10)快,该怎么办?如果我希望给它一个长达1分钟5分钟的机会怎么办?如果我的脚本中有很多这样的情况怎么办:)
it3xl

3

我也有这个问题,发现另外两件事非常有用:

  1. bash中的SECONDS变量。
  2. 命令“ pgrep”。

因此,我在命令行(OSX 10.9)上使用了类似的方法:

ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done

由于这是一个循环,因此我添加了“ sleep 0.2”以保持CPU冷却。;-)

(顺便说一句:ping是一个不好的例子,您只需要使用内置的“ -t”(超时)选项即可。)


1

假设您拥有(或可以轻松地制作)一个用于跟踪孩子的pid的pid文件,则可以创建一个脚本来检查pid文件的modtime并根据需要终止/重生该进程。然后只需将脚本放入crontab中即可在所需的时间运行。

让我知道您是否需要更多详细信息。如果这听起来不符合您的需求,那么新贵呢?


1

一种方法是在子外壳程序中运行程序,并使用read命令通过命名管道与子外壳程序通信。这样,您可以检查正在运行的进程的退出状态,并将其通过管道传达回去。

这是一个yes在3秒钟后使命令超时的示例。它使用获取进程的PID pgrep(可能仅在Linux上有效)。使用管道还存在一些问题,即打开用于读取的管道的进程将挂起,直到它也被打开以进行写入,反之亦然。因此,为防止read命令挂起,我“楔住”了一个管道,以便使用背景子外壳进行读取。(另一种防止冻结的操作以读写方式打开管道的方法,即read -t 5 <>finished.pipe,但是,除了Linux,这也可能不起作用。)

rm -f finished.pipe
mkfifo finished.pipe

{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!

# Get command PID
while : ; do
    PID=$( pgrep -P $SUBSHELL yes )
    test "$PID" = "" || break
    sleep 1
done

# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &  

read -t 3 FINISHED <finished.pipe

if [ "$FINISHED" = finished ] ; then
  echo 'Subprocess finished'
else
  echo 'Subprocess timed out'
  kill $PID
fi

rm finished.pipe

0

这是一种尝试避免退出的进程的尝试,这减少了杀死具有相同进程ID的另一个进程的可能性(尽管可能无法完全避免此类错误)。

run_with_timeout ()
{
  t=$1
  shift

  echo "running \"$*\" with timeout $t"

  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid

  # the timeout shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter

  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter

     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi

     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting shell
       kill $waiter
       exit $status
     fi
  )
}

使用like run_with_timeout 3 sleep 10000,它会运行,sleep 10000但会在3秒钟后结束。

就像其他答案一样,这些答案使用后台超时进程在延迟后杀死子进程。我认为这与Dan的扩展答案(https://stackoverflow.com/a/5161274/1351983)几乎相同,除了如果超时shell已经结束,它不会被杀死。

该程序结束后,仍将有一些挥之不去的“睡眠”进程,但它们应该无害。

这可能比我的其他答案更好,因为它不使用非便携式外壳功能read -t,也不使用pgrep


(exec sh -c "$*") &和之间有什么区别sh -c "$*" &?具体来说,为什么要使用前者而不是后者?
贾斯汀C

0

这是我在这里提交的第三个答案。当SIGINT接收到信号时,这将处理信号中断并清理后台进程。它使用最上面的答案中$BASHPIDexec技巧获取进程的PID(在本例中为调用)。它使用FIFO与负责终止和清理的子Shell通信。(这就像我第二个答案中的管道一样,但是拥有一个命名管道意味着信号处理程序也可以写入其中。)$$sh

run_with_timeout ()
{
  t=$1 ; shift

  trap cleanup 2

  F=$$.fifo ; rm -f $F ; mkfifo $F

  # first, run main process in background
  "$@" & pid=$!

  # sleeper process to time out
  ( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
  read sleeper <$F

  # control shell. read from fifo.
  # final input is "finished".  after that
  # we clean up.  we can get a timeout or a
  # signal first.
  ( exec 0<$F
    while : ; do
      read input
      case $input in
        finished)
          test $sleeper != 0 && kill $sleeper
          rm -f $F
          exit 0
          ;;
        timeout)
          test $pid != 0 && kill $pid
          sleeper=0
          ;;
        signal)
          test $pid != 0 && kill $pid
          ;;
      esac
    done
  ) &

  # wait for process to end
  wait $pid
  status=$?
  echo finished >$F
  return $status
}

cleanup ()
{
  echo signal >$$.fifo
}

我已尽力避免出现比赛情况。但是,我无法消除的一个错误源是进程在接近超时的同时结束。例如,run_with_timeout 2 sleep 2run_with_timeout 0 sleep 0。对我来说,后者给出了一个错误:

timeout.sh: line 250: kill: (23248) - No such process

因为它试图杀死一个已经退出的进程。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.