如何在Bash中给定超时时间后终止子进程?

199

我有一个bash脚本,它启动了一个子进程,但是这个子进程会不时地崩溃(实际上是挂起),而且没有明显的原因(闭源,所以我无能为力)。因此,我希望能够在给定的时间内启动此进程,并在给定的时间后如果未成功返回则将其终止。

是��有一种简单稳健的方法可以使用bash实现这一点?


相关链接:https://dev59.com/i3RB5IYBdhLWcg3wgXdV - pilcrow
非常完整的回答在此处:https://stackoverflow.com/a/58873049/2635443 - Orsiris de Jong
9个回答

299

(如下所示: BASH FAQ entry #68: "How do I run a command, and have it abort (timeout) after N seconds?")

您可以使用timeout*:

timeout 10 ping www.goooooogle.com

否则,执行 timeout 内部的操作:
( 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 )

* 它已经包含在GNU Coreutils 8+中,因此大多数当前的Linux系统已经安装了它,否则您可以安装它,例如sudo apt-get install timeoutsudo apt-get install coreutils


12
如果其他人想知道我的做法,请参考Ignacio的回复:cmdpid=$BASHPID将不会获取调用 shell 的 pid,而是由 () 启动的(第一个)子 shell 的 pid。 (sleep...的代码段在第一个子 shell 中调用第二个子 shell,在后台等待10秒钟并杀死第一个子 shell,然后第一个子 shell 在启动了杀手子 shell 进程之后继续执行其工作负载... - jamadagni
19
timeout 是 GNU coreutils 的一部分,因此应该已经安装在所有的 GNU 系统中。 - Sameer
1
@Sameer:仅限于版本8及以上。 - Ignacio Vazquez-Abrams
3
我不是100%确定,但据我所知(我知道我的manpage告诉我的),timeout现在是coreutils的一部分。 - benaryorg
6
这个命令不会“提前完成”。它总是会在超时时终止进程,但不会处理没有超时的情况。 - hawkeye
显示剩余4条评论

37
# 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=$?

9
在尝试使用kill -9之前,你应该先尝试进程可以处理的信号。 - Dennis Williamson
没错,我想要一个快速的解决方案,因此只是假设他希望该进程立即停止,因为他说它会崩溃。 - Dan
10
这实际上是一个很糟糕的解决方案。如果dosmth在2秒内终止,另一个进程会获取旧的pid,并且你会杀死新的进程? - Teleporting Goat
2
PID回收通过达到极限并循环使用来实现。除非系统完全失控,否则另一个进程在剩余的8秒内重新使用PID的可能性非常小。 - kittydoor

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

它会产生过多的等待时间。如果真实的命令(在这里为 sleep 999)经常比强制休眠时间(sleep 10)更快地完成,该怎么办?如果我希望它有机会延长到1分钟、5分钟呢?如果我的脚本中有一堆这样的情况怎么办 :) - it3xl

4

我也有这个问题,并找到了另外两个非常有用的东西:

  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
一种方法是在子shell中运行程序,并通过具有read命令的命名管道与子shell通信。这样,您可以检查正在运行的进程的退出状态,并通过管道将其传递回来。
以下是超时3秒后停止yes命令的示例。它使用pgrep获取进程的PID(可能仅适用于Linux)。在使用管道时还存在一些问题,即打开读取管道的进程将挂起,直到也打开写入管道,反之亦然。因此,为了防止read命令挂起,我已经使用后台子shell“卡住”了读取管道。 (另一种防止冻结的方法是打开管道读写,即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

1

假设您已经有(或可以轻松创建)用于跟踪子进程pid的pid文件,然后您可以创建一个脚本来检查pid文件的修改时间,并根据需要杀死/重新启动进程。然后只需将脚本放入crontab中,在大约需要的时间段运行即可。

如果您需要更多细节,请告诉我。如果那听起来不适合您的需求,那么upstart?怎么样?


0
这是我在这里提交的第三个答案。当接收到SIGINT时,此答案处理信号中断并清理后台进程。它使用了最佳答案中使用的$BASHPIDexec技巧来获取进程的PID(在这种情况下,在sh调用中的$$)。它使用FIFO与负责杀死和清理的子shell通信。(这类似于我第二个答案中的管道,但具有命名管道意味着信号处理程序也可以写入其中。)
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

因为它试图终止一个已经自行退出的进程。


0
#Kill command after 10 seconds
timeout 10 command

#If you don't have timeout installed, this is almost the same:
sh -c '(sleep 10; kill "$$") & command'

#The same as above, with muted duplicate messages:
sh -c '(sleep 10; kill "$$" 2>/dev/null) & command'

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
  )
}

使用类似于run_with_timeout 3 sleep 10000的方式,它运行sleep 10000但在3秒后结束。

这就像其他答案一样,使用后台超时进程在延迟后终止子进程。我认为这几乎与丹的扩展答案(https://dev59.com/8W435IYBdhLWcg3w4UMc#5161274)相同,除了如果已经结束,则不会杀死超时shell。

在此程序结束后,仍将有一些挂起的“sleep”进程运行,但它们应该是无害的。

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


“(exec sh -c "$") &”和“sh -c "$" &”有什么区别?具体来说,为什么要使用前者而不是后者?” - Justin C

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