在bash中使命令超时而没有不必要的延迟


283

命令行命令的此答案在一定时间后自动杀死命令

提出了一种1行方法来使bash命令行中的长时间运行的命令超时:

( /path/to/slow command with options ) & sleep 5 ; kill $!

但是给定的“长时间运行”命令有可能在超时之前完成。(我们称其为“通常长时间运行,但有时又快”的命令,或以tlrbsf为乐。)

因此,这种漂亮的1线方法存在两个问题。首先,此sleep条件不是有条件的,因此会为序列完成设置所需的时间下限。当tlrbsf命令在2秒内完成时,请考虑30s或2m甚至5m的睡眠时间,这是非常不希望的。其次,kill是无条件的,因此此序列将尝试杀死一个未运行的进程并对其进行抱怨。

所以...

有没有办法使通常运行时间长但有时又快的命令(“ tlrbsf”超时

  • 有一个bash实现(另一个问题已经有Perl和C答案)
  • 将在以下两者中的较早者终止:tlrbsf程序终止或超时
  • 不会杀死不存在/无法运行的进程(或者(可选):不会抱怨严重的失败)
  • 不必是1班轮
  • 可以在Cygwin或Linux下运行

...并且为了获得奖励积分,在前台运行tlrbsf命令,在后台运行任何“睡眠”或额外的进程,以便可以重定向tlrbsf命令的stdin / stdout / stderr ,就像之前那样直接运行?

如果是这样,请分享您的代码。如果没有,请解释原因。

我花了一段时间尝试破解前面提到的例子,但是我达到了bash技能的极限。


5
另一个类似的问题:stackoverflow.com/questions/526782/…(但我认为这里的“ timeout3”答案要好得多)。
系统暂停

2
有什么理由不使用gnu timeout实用程序?
克里斯·约翰逊

timeout是很棒的!您甚至可以使用多个命令(多行脚本):stackoverflow.com/a/61888916/658497
Noam Manos

Answers:


149

我认为这正是您要的:

http://www.bashcookbook.com/bashinfo/source/bash-4.0/examples/scripts/timeout3

#!/bin/bash
#
# The Bash shell script executes a command with a time-out.
# Upon time-out expiration SIGTERM (15) is sent to the process. If the signal
# is blocked, then the subsequent SIGKILL (9) terminates it.
#
# Based on the Bash documentation example.

# Hello Chet,
# please find attached a "little easier"  :-)  to comprehend
# time-out example.  If you find it suitable, feel free to include
# anywhere: the very same logic as in the original examples/scripts, a
# little more transparent implementation to my taste.
#
# Dmitry V Golovashkin <Dmitry.Golovashkin@sas.com>

scriptName="${0##*/}"

declare -i DEFAULT_TIMEOUT=9
declare -i DEFAULT_INTERVAL=1
declare -i DEFAULT_DELAY=1

# Timeout.
declare -i timeout=DEFAULT_TIMEOUT
# Interval between checks if the process is still alive.
declare -i interval=DEFAULT_INTERVAL
# Delay between posting the SIGTERM signal and destroying the process by SIGKILL.
declare -i delay=DEFAULT_DELAY

function printUsage() {
    cat <<EOF

Synopsis
    $scriptName [-t timeout] [-i interval] [-d delay] command
    Execute a command with a time-out.
    Upon time-out expiration SIGTERM (15) is sent to the process. If SIGTERM
    signal is blocked, then the subsequent SIGKILL (9) terminates it.

    -t timeout
        Number of seconds to wait for command completion.
        Default value: $DEFAULT_TIMEOUT seconds.

    -i interval
        Interval between checks if the process is still alive.
        Positive integer, default value: $DEFAULT_INTERVAL seconds.

    -d delay
        Delay between posting the SIGTERM signal and destroying the
        process by SIGKILL. Default value: $DEFAULT_DELAY seconds.

As of today, Bash does not support floating point arithmetic (sleep does),
therefore all delay/time values must be integers.
EOF
}

# Options.
while getopts ":t:i:d:" option; do
    case "$option" in
        t) timeout=$OPTARG ;;
        i) interval=$OPTARG ;;
        d) delay=$OPTARG ;;
        *) printUsage; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

# $# should be at least 1 (the command to execute), however it may be strictly
# greater than 1 if the command itself has options.
if (($# == 0 || interval <= 0)); then
    printUsage
    exit 1
fi

# kill -0 pid   Exit code indicates if a signal may be sent to $pid process.
(
    ((t = timeout))

    while ((t > 0)); do
        sleep $interval
        kill -0 $$ || exit 0
        ((t -= interval))
    done

    # Be nice, post SIGTERM first.
    # The 'exit 0' below will be executed if any preceeding command fails.
    kill -s SIGTERM $$ && kill -0 $$ || exit 0
    sleep $delay
    kill -s SIGKILL $$
) 2> /dev/null &

exec "$@"

这是一个敏锐的技巧,在背景部分使用$$。很好,它将立即杀死挂死的tlrbsf。但是,您必须选择一个轮询间隔。而且,如果将轮询设置得太低,它将以恒定的信号消耗CPU,使tlrbsf的运行时间更长!
系统暂停

7
您不必选择轮询间隔,它的默认值为1s,这非常好。而且检查非常便宜,开销可忽略不计。我怀疑这会使tlrbsf的运行时间明显更长。我测试了30次睡眠,使用和未使用之间有0.000ms的差异。
朱利诺

6
是的,我现在看到了。如果您将轮询间隔设置为==超时,则可以满足我的确切要求。还可以在管道中工作,在整个背景下工作,在多个实例和其他正在运行的作业中工作。亲爱的,谢谢!
系统暂停

发送信号会杀死该子外壳,因此我认为将所有kill命令排成一行会保留它们。我还启用了stderr输出以显示意外错误。 stackoverflow.com/questions/687948/...
鳗鱼ghEEz

1
@Juliano这是处理超时的好方法,非常有用。我想知道是否有一种方法可以让我们在超时后终止该进程时使脚本返回exitcode 143?我尝试在kill命令之后立即添加“ exit 143”,但在调用者脚本中总是得到退出代码0。
Salman A. Kagzi 2013年

528

您可能正在timeoutcoreutils中寻找命令。由于它是coreutils的一部分,因此从技术上讲它是C解决方案,但仍然是coreutils。info timeout更多细节。这是一个例子:

timeout 5 /path/to/slow/command with options

21
在Mac中,您可以通过Macports或自制软件安装。
Ivan Z. Siu 2012年

23
通过OS X上的自制软件安装后,该命令将变为gtimeout
ethicalhack3r,2013年

5
...您使用的是2003之前的具有coreutils的操作系统?
Keith 2013年

5
@Keith:例如CentOS 5.10 :-(
已暂停,直至另行通知。

9
为了明确起见,在OSX / mac上需要的homebrew命令是brew install coreutils,然后可以使用gtimeout
Ohad Schneider

37

无论bash监控器模式如何,此解决方案都可以工作。您可以使用适当的信号来终止your_command

#!/bin/sh
( your_command ) & pid=$!
( sleep $TIMEOUT && kill -HUP $pid ) 2>/dev/null & watcher=$!
wait $pid 2>/dev/null && pkill -HUP -P $watcher

观察者在给定的超时后杀死your_command;脚本等待慢的任务并终止观察程序。请注意,这wait不适用于属于不同外壳的子进程。

例子:

  • your_command运行超过2秒并被终止

your_command中断

( sleep 20 ) & pid=$!
( sleep 2 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "your_command finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "your_command interrupted"
fi
  • your_command在超时(20秒)之前完成

your_command完成

( sleep 2 ) & pid=$!
( sleep 20 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "your_command finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "your_command interrupted"
fi

1
wait返回其等待的进程的退出状态。因此,如果您的命令确实在分配的时间内退出但退出状态为非零,则此处的逻辑将按照其超时的方式运行,即print your_command interrupted。相反,您可以不使用waitan进行操作if,然后检查$watcherpid是否仍然存在,如果存在,则说明您没有超时。
乔治·霍金斯

我不知道为什么kill在一种情况下使用它,而pkill在另一种情况下使用。我需要同时使用pkill两者才能使其正常工作。我希望如果将命令包装在中(),那么您将需要使用pkill它来杀死它。但是,如果在.NET中只有一个命令,则可能会有所不同()
nobar '16

上帝保佑您,先生:)
Darko Miletic,

22

你去了:

timeout --signal=SIGINT 10 /path/to/slow command with options

您可以根据需要更改SIGINT10;)


3
“超时”是(至少)在Redhat,Centos,Suse和Ubuntu上的coreutils软件包的一部分,因此,如果没有,则需要安装它。
Akom

这真的很有帮助!您知道为什么yingted的“超时5 / path / to / slow / command with options”有时不起作用吗?
Decula

不幸的是,这不在coreutilsFreeBSD 的软件包中。
Paul Bissex

18

您可以使用以下方法完全做到这一点bash 4.3

_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; r=$?; kill -9 `jobs -p`; exit $r; ) }
  • 例: _timeout 5 longrunning_command args
  • 例: { _timeout 5 producer || echo KABOOM $?; } | consumer
  • 例: producer | { _timeout 5 consumer1; consumer2; }
  • 例: { while date; do sleep .3; done; } | _timeout 5 cat | less

  • 需要Bash 4.3 wait -n

  • 如果命令被杀死,则给出137,否则返回命令的返回值。
  • 适用于管道。(您不需要在这里成为前台!)
  • 也可以与内部Shell命令或功能一起使用。
  • 在子外壳程序中运行,因此无法将变量导出到当前外壳程序中。

如果您不需要返回码,则可以使其更简单:

_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; kill -9 `jobs -p`; ) }

笔记:

  • 严格来说,您不需要;in ; ),但是它使事情与; }-case 更加一致。而且set +b可能也会被遗弃,但是比后悔更安全。

  • 除了--forground(可能),您可以实现所有变体timeout支持。 --preserve-status不过有点困难。这留给读者练习;)

此配方可以在外壳中“自然”使用(与一样自然flock fd):

(
set +b
sleep 20 &
{
YOUR SHELL CODE HERE
} &
wait -n
kill `jobs -p`
)

但是,如上所述,您不能自然地将环境变量重新导出到封闭的外壳程序中。

编辑:

实际示例:__git_ps1万一花费太长时间(对于慢速SSHFS-Link之类的东西)超时:

eval "__orig$(declare -f __git_ps1)" && __git_ps1() { ( git() { _timeout 0.3 /usr/bin/git "$@"; }; _timeout 0.3 __orig__git_ps1 "$@"; ) }

编辑2:错误修正。我注意到这exit 137是不必要的,并且同时使其_timeout不可靠。

Edit3:git是一个顽固的家伙,因此需要双重技巧才能令人满意地工作。

Edit4:_第一次_timeout在真实的GIT示例中忘记了一个。


1
重击4块岩石。就这些。
系统

1
这实际上需要Bash 4.3或更高版本。 cc. The 'wait' builtin has a new '-n' option to wait for the next child to change status. 来自:tiswww.case.edu/php/chet/bash/NEWS
Ben

17

我更喜欢“ timelimit”,它至少在debian中有一个包。

http://devel.ringlet.net/sysutils/timelimit/

它比coreutils“ timeout”好一点,因为它在终止进程时会打印一些内容,并且默认情况下还会在一段时间后发送SIGKILL。


它似乎不能很好地工作:/ $ time timelimit -T2 sleep 10 real 0m10.003s user 0m0.000s sys 0m0.000s
hithwen 2012年

3
使用-t2而不是-T2。大-T是从发送SIGTERM到发送SIGKILL的时间。
maxy 2012年

1
我想补充一点,就是timelimit 1.8不能与fork完美兼容(timelimit -t1 ./a_forking_prog仅杀死两个进程之一),但是超时有效。
Jeremy Cochoy

如果您希望在终止进程时超时打印某些内容,只需使用“ -v”标志。

10

slowcommand在1秒后超时:

timeout 1 slowcommand || echo "I failed, perhaps due to time out"

要确定命令是出于自身原因超时还是失败,请检查状态码是否为124:

# ping for 3 seconds, but timeout after only 1 second
timeout 1 ping 8.8.8.8 -w3
EXIT_STATUS=$?
if [ $EXIT_STATUS -eq 124 ]
then
echo 'Process Timed Out!'
else
echo 'Process did not timeout. Something else went wrong.'
fi
exit $EXIT_STATUS

请注意,当退出状态为124时,您不知道它是否由于timeout命令而超时,或者命令本身是否由于自身的某些内部超时逻辑而终止,然后返回124。无论哪种情况,您都可以放心地假设,但是,发生了某种超时。



8

Kinda hacky,但是可以用。如果您还有其他前台进程,则不起作用(请帮助我解决此问题!)

sleep TIMEOUT & SPID=${!}; (YOUR COMMAND HERE; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}

实际上,我认为您可以逆转它,达到您的“奖励”标准:

(YOUR COMMAND HERE & SPID=${!}; (sleep TIMEOUT; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}) < asdf > fdsa

(ls -ltR / cygdrive / c / windows和SPID = $ {!};(睡眠1秒钟;杀死$ {SPID})和CPID = $ {!}; fg 1;杀死$ {CPID})> fdsa
系统暂停

bash:fg:无作业控制
系统暂停

我认为@system暂停,设置-m。
斯特拉格

我在登录外壳中具有作业控制(设置-m)。那是$-的himBH内容中的“ m”,但是对于子外壳它似乎消失了。可能是Cygwin工件。抱怨
系统暂停

不要在脚本中使用“ fg”。阅读“帮助等待”。
lhunath

8

超时可能是尝试的第一种方法。如果超时,您可能需要通知或其他命令才能执行。经过大量搜索和试验后,我想到了以下bash脚本:

if 
    timeout 20s COMMAND_YOU_WANT_TO_EXECUTE;
    timeout 20s AS_MANY_COMMANDS_AS_YOU_WANT;
then
    echo 'OK'; #if you want a positive response
else
    echo 'Not OK';
    AND_ALTERNATIVE_COMMANDS
fi

5

具有代码清晰性的简单脚本。保存到/usr/local/bin/run

#!/bin/bash

# run
# Run command with timeout $1 seconds.

# Timeout seconds
timeout_seconds="$1"
shift

# PID
pid=$$

# Start timeout
(
  sleep "$timeout_seconds"
  echo "Timed out after $timeout_seconds seconds"
  kill -- -$pid &>/dev/null
) &
timeout_pid=$!

# Run
"$@"

# Stop timeout
kill $timeout_pid &>/dev/null

使运行时间过长的命令超时:

$ run 2 sleep 10
Timed out after 2 seconds
Terminated
$

立即结束,以完成以下命令:

$ run 10 sleep 2
$

3

如果您已经知道程序名称(假设program)在超时后终止(例如3秒),那么我可以提供一个简单且有点脏的替代解决方案:

(sleep 3 && killall program) & ./program

如果我通过系统调用来调用基准测试流程,则此方法非常有效。


2
这会杀死其他碰巧使用该名称的进程,并且如果它更改了名称,也不会杀死具有给定名称的进程(例如,通过写入argv[0],或者通过其他黑客手段来腾出更多空间)。
2012年

尝试在一定时间后停止Docker容器时,我发现了这种方便的变体。泊坞窗客户端似乎并未接受TERM / INT / KILL,实际上不会停止在前台运行的容器。因此,给容器起一个名称并使用它(sleep 3 && docker stop <container>) &效果很好。谢谢!
马特·沙弗

是的,它很脏而且很有限,但是可以通过以下{ sleep 5 && kill -9 $(ps -fe | grep "program" | grep $$ | tr -s " " | cut -d" " -f2); } & SLEEPPID=$!; bash -c "program" && kill -9 $SLEEPPID"方式进行改进:这样,它将仅杀死当前shell中的作业。

2

cratimeoutMartin Cracauer 也有(用C语言为Unix和Linux系统编写)。

# cf. http://www.cons.org/cracauer/software.html
# usage: cratimeout timeout_in_msec cmd args
cratimeout 5000 sleep 1
cratimeout 5000 sleep 600
cratimeout 5000 tail -f /dev/null
cratimeout 5000 sh -c 'while sleep 1; do date; done'

“特别是,它保留了信号行为。” 很高兴有这个选择!
系统暂停了

1

OS X还没有使用bash 4,也没有/ usr / bin / timeout,所以这是一个在OS X上运行的函数,没有Home-brew或macport,类似于/ usr / bin / timeout(基于Tino的回答)。参数验证,帮助,用法以及对其他信号的支持是读者的一项练习。

# implement /usr/bin/timeout only if it doesn't exist
[ -n "$(type -p timeout 2>&1)" ] || function timeout { (
    set -m +b
    sleep "$1" &
    SPID=${!}
    ("${@:2}"; RETVAL=$?; kill ${SPID}; exit $RETVAL) &
    CPID=${!}
    wait %1
    SLEEPRETVAL=$?
    if [ $SLEEPRETVAL -eq 0 ] && kill ${CPID} >/dev/null 2>&1 ; then
      RETVAL=124
      # When you need to make sure it dies
      #(sleep 1; kill -9 ${CPID} >/dev/null 2>&1)&
      wait %2
    else
      wait %2
      RETVAL=$?
    fi
    return $RETVAL
) }

0

在99%的情况下,答案是不实施任何超时逻辑。在几乎任何情况下,超时逻辑都是红色的警告信号,表示发生了其他错误,应该改正

有时n秒后您的过程是否挂起或中断?然后找出原因并解决该问题。

顺便说一句,要正确执行strager的解决方案,您需要使用wait“ $ SPID”而不是fg 1,因为在脚本中您没有作业控制(并且试图将其打开是愚蠢的)。而且,fg 1依赖于您以前没有在脚本中启动任何其他作业的事实,这是一个糟糕的假设。


4
通过访问100%的源(以及大多数硬件,例如网络交换机),我同意可能有比超时更好的解决方案。但是,当“ tlrbsf”是封闭源代码(仅二进制)时,有时您必须解决该限制。
系统暂停

@lhunath,“在脚本中你没有作业控制(并试图打开它是愚蠢的)” -请这里澄清:stackoverflow.com/questions/690266/...
系统暂停

@系统暂停:回复stackoverflow.com/questions/690266/…是正确的,我对此也发表了评论。
lhunath

29
笨拙,你的意思说不通。在很多情况下,超时是一个不错的选择,例如,任何时候您都必须通过网络访问。
Nate Murray

一个例外:您正在编写测试脚本以检查已经运行的软件是否没有超时问题。
彼得-恢复莫妮卡

0

我遇到了一个问题,要保留shell上下文并允许超时,它的唯一问题是它将在超时时停止脚本执行-但是满足我的需求就可以了:

#!/usr/bin/env bash

safe_kill()
{
  ps aux | grep -v grep | grep $1 >/dev/null && kill ${2:-} $1
}

my_timeout()
{
  typeset _my_timeout _waiter_pid _return
  _my_timeout=$1
  echo "Timeout($_my_timeout) running: $*"
  shift
  (
    trap "return 0" USR1
    sleep $_my_timeout
    echo "Timeout($_my_timeout) reached for: $*"
    safe_kill $$
  ) &
  _waiter_pid=$!
  "$@" || _return=$?
  safe_kill $_waiter_pid -USR1
  echo "Timeout($_my_timeout) ran: $*"
  return ${_return:-0}
}

my_timeout 3 cd scripts
my_timeout 3 pwd
my_timeout 3 true  && echo true || echo false
my_timeout 3 false && echo true || echo false
my_timeout 3 sleep 10
my_timeout 3 pwd

输出:

Timeout(3) running: 3 cd scripts
Timeout(3) ran: cd scripts
Timeout(3) running: 3 pwd
/home/mpapis/projects/rvm/rvm/scripts
Timeout(3) ran: pwd
Timeout(3) running: 3 true
Timeout(3) ran: true
true
Timeout(3) running: 3 false
Timeout(3) ran: false
false
Timeout(3) running: 3 sleep 10
Timeout(3) reached for: sleep 10
Terminated

我当然认为有一个目录 scripts


0
#! /bin/bash
timeout=10
interval=1
delay=3
(
    ((t = timeout)) || :

    while ((t > 0)); do
        echo "$t"
        sleep $interval
        # Check if the process still exists.
        kill -0 $$ 2> /dev/null || exit 0
        ((t -= interval)) || :
    done

    # Be nice, post SIGTERM first.
    { echo SIGTERM to $$ ; kill -s TERM $$ ; sleep $delay ; kill -0 $$ 2> /dev/null && { echo SIGKILL to $$ ; kill -s KILL $$ ; } ; }
) &

exec "$@"

@Tino抱歉,我忘记了为什么更改流程终止行以及为什么我认为共享很重要。太糟糕了,我没有写下来。也许,我发现我需要在检查kill -s TERM成功之前需要暂停一下。烹饪书中的2008年脚本似乎在发送SIGTERM之后立即检查进程的状态,这可能导致尝试将SIGKILL发送到已死的进程时出错。
eel ghEEz 2015年

0

我的问题可能有所不同:我在远程计算机上通过ssh启动命令,并希望在命令挂起时杀死shell和childs。

我现在使用以下内容:

ssh server '( sleep 60 && kill -9 0 ) 2>/dev/null & my_command; RC=$? ; sleep 1 ; pkill -P $! ; exit $RC'

这样,当超时时命令将返回255或在成功的情况下返回命令的返回码

请注意,从ssh会话中终止进程的处理方式不同于交互式shell。但是您也可以使用-t选项来ssh分配伪终端,因此它的作用类似于交互式shell


0

这是一个不依赖于生成子进程的版本-我需要一个嵌入了此功能的独立脚本。它还会执行分数轮询间隔,因此您可以更快地轮询。超时本来是首选-但我被困在旧服务器上

# wait_on_command <timeout> <poll interval> command
wait_on_command()
{
    local timeout=$1; shift
    local interval=$1; shift
    $* &
    local child=$!

    loops=$(bc <<< "($timeout * (1 / $interval)) + 0.5" | sed 's/\..*//g')
    ((t = loops))
    while ((t > 0)); do
        sleep $interval
        kill -0 $child &>/dev/null || return
        ((t -= 1))
    done

    kill $child &>/dev/null || kill -0 $child &>/dev/null || return
    sleep $interval
    kill -9 $child &>/dev/null
    echo Timed out
}

slow_command()
{
    sleep 2
    echo Completed normally
}

# wait 1 sec in 0.1 sec increments
wait_on_command 1 0.1 slow_command

# or call an external command
wait_on_command 1 0.1 sleep 10

-1

一种非常简单的方法:

# command & sleep 5; pkill -9 -x -f "command"

使用pkill(选项-f)可以杀死带有参数的特定命令,或者指定-n以避免杀死旧进程。


您意识到这实际上是OP在他的职位中所拥有的,并且他表示他不希望这样做,对吗?因为它总是等待完整的睡眠延迟。
伊坦·赖斯纳

-1

@loup的答案为基础 ...

如果要使进程超时并使kill job / pid输出静音,请运行:

( (sleep 1 && killall program 2>/dev/null) &) && program --version 

这会将后台进程放入子外壳中,因此您看不到作业输出。


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.