在bash中,如何在不必要的延迟情况下超时执行命令?

377

这个答案针对命令行命令在一定时间后自动终止执行提出了一种一行代码的方法,用于从bash命令行超时终止长时间运行的命令:

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

但是一个长时间运行的命令可能会在超时之前完成。
(我们称其为“通常长时间运行但有时很快”的命令,或者为了好玩而称之为tlrbsf。)

因此,这种巧妙的一行代码方法存在几个问题。
首先,sleep不是条件性的,因此它设置了序列完成所需的不良下限时间。当tlrbsf命令在2秒内完成时,请考虑30秒、2分钟甚至5分钟的睡眠时间- 这是非常不理想的。
其次,kill是无条件的,因此该序列将尝试杀死一个非运行进程并抱怨。

所以...

是否有一种方法可以超时一个通常长时间运行但有时很快("tlrbsf")的命令,使其

  • 具有bash实现(其他问题已经有Perl和C的答案)
  • 将在以下两者中较早终止:tlrbsf程序终止,或超时时间到期
  • 不会杀死不存在/未运行的进程(或者,可选:不会抱怨错误的杀死)
  • 不必是1行代码
  • 可以在Cygwin或Linux下运行

...并且,额外加分项:

  • 在前台中运行tlrbsf命令
  • 任何“睡眠”或额外进程在后台运行

这样,tlrbsf 命令的标准输入/输出/错误可以被重定向,就像直接运行一样吗?

如果是这样,请分享你的代码。如果不是,请解释原因。

我已经花了一段时间尝试破解上述示例,但我达到了我的 Bash 技能极限。


5
另一个类似的问题:https://dev59.com/gXRB5IYBdhLWcg3wv5o_(但我认为这里的“timeout3”答案更好)。 - system PAUSE
2
有没有不使用GNU timeout实用程序的原因? - Chris Johnson
2
timeout 真是太棒了!你甚至可以将其用于 多个命令(多行脚本):https://dev59.com/EloT5IYBdhLWcg3w8jJm#61888916 - Noam Manos
24个回答

676

你可能在寻找 coreutils 中的 timeout 命令。它虽然是 coreutils 的一部分,因此从技术上讲可以算作 C 语言解决方案,但它仍然是 coreutils。有关更多详细信息,请参见 info timeout

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

28
在Mac系统中,您可以通过Macports或Homebrew来安装此软件。 - Ivan Xiao
27
在OS X上通过Homebrew安装后,该命令变成gtimeout - ethicalhack3r
6
你使用的操作系统中,是否安装了早于2003年发布的 coreutils? - Keith
5
@Keith: 例如 CentOS 5.10 :-( - Dennis Williamson
17
需要在 MacOS 上使用的Homebrew命令是 brew install coreutils,然后你就可以使用 gtimeout。请注意,这是为了澄清问题。 - Ohad Schneider
显示剩余10条评论

170
我想这恰好是您所要求的内容:

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


(该链接指向一个关于 Bash 脚本的计时器的示例代码,可用于限制脚本执行时间。)
#!/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 "$@"

9
您无需选择轮询间隔,因为它有默认的1秒轮询间隔,这非常好。检查操作非常廉价,开销可以忽略不计。我怀疑这不会显著延长 tlrbsf 的运行时间。我使用 sleep 30 进行测试,并在使用和不使用时得到了0.000ms的差异。 - Juliano
7
好的,我现在理解了。如果将轮询间隔设置为超时时间,它就符合我的精确要求。它可以在管道中工作,在整个后台运行中工作,可以与多个实例和其他正在运行的作业一起工作。太好了,谢谢! - system PAUSE
1
发送信号会杀死子shell,所以我想把所有的kill命令放在一行上,这样可以保留它们。我还启用了stderr输出来显示意外的错误。 - eel ghEEz
2
@Juliano 这是处理超时的好方法,非常有用。 我想知道是否有一种方法可以在进程在超时后被终止时使脚本返回退出代码143? 我尝试在kill命令之后添加“exit 143”,但我总是在调用脚本时得到退出代码0。 - Salman A. Kagzi
在最后需要'''exec'''吗?我想将这段代码用作函数,但是在函数中无法使用exec,因为执行命令后,exec不会返回。 - wackazong
显示剩余4条评论

48
这个解决方案适用于无论是何种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

这个"watcher"会在指定的时间后关闭你的命令;脚本等待执行缓慢任务并终止"watcher"。请注意,wait不能用于其他Shell下的子进程。

示例:

  • 你的命令运行超过2秒钟并被终止

你的命令被中断了

( 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
  • 你的命令在超时时间(20秒)之前完成了

你的命令已经完成

( 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

2
wait 返回它等待的进程的退出状态。因此,如果您的命令在分配的时间内退出,但具有非零退出状态,则此处的逻辑将表现为超时,即打印“your_command interrupted”。相反,您可以在没有 if 的情况下执行 wait,然后检查 $watcher pid 是否仍存在,如果存在,则知道您没有超时。 - George Hawkins
1
我无法弄清楚为什么在一个情况下使用kill,而在另一个情况下使用pkill。我需要同时使用pkill才能使其正常工作。我原本以为如果你将一个命令包装在()中,那么你需要使用pkill来杀死它。但是,如果()中只有一个命令,那么它可能会有不同的工作方式。 - Brent Bradburn
神保佑您先生 :) - Darko Miletic

35

为了在1秒后超时slowcommand:

timeout 1 slowcommand || echo "我失败了,可能是因为超时了"

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

# ping the address 8.8.8.8 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。不过,在任何情况下,您都可以安全地假设发生了某种超时。


29

就是这样:

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

你可以根据自己的需要更改 SIGINT10 ;)


3
"If you don't have it, you'll need to install the coreutils package, which includes "timeout", on Redhat, Centos, Suse, and Ubuntu at least." - Akom
这真的很有帮助!!!你知道为什么yingted的“timeout 5 /path/to/slow/command with options”有时候不起作用吗? - Decula
很遗憾,在FreeBSD上coreutils包中没有这个功能。 - Paul Bissex

21
你可以完全使用及以上版本来完成这个任务。
_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 命令或函数。
  • 在子 shell 中运行,因此无法导出变量到当前 shell,抱歉。

如果您不需要返回代码,这可以更加简单:

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

注意:

  • 严格来说,在 ;) 中不需要 ;,但是为了与 ;} 保持一致,加上它会更好。而且set +b可能也可以省略,但是安全起见最好留着。

  • 除了 --forground(可能)之外,你可以实现 timeout 支持的所有变体。然而,--preserve-status 有点困难。这留给读者作为练习吧 ;)

这个方法可以在 Shell 中“自然地”使用(就像对于 flock fd 一样):

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

然而,正如上面所解释的那样,你不能以自然的方式重新导出环境变量到封闭的shell中。
编辑:
现实世界的例子:如果花费太长时间(例如慢速SSHFS-Links),则超时__git_ps1
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变得不可靠。

编辑3:git是一个顽强的家伙,所以需要双重技巧才能令其令人满意地工作。

编辑4:在真实世界的GIT示例中,忘记了第一个_timeout中的_


更新2023-08-06:我找到了一种更好的方法来限制git的运行时间,所以上面只是一个例子。

以下不再仅限于bash,因为它需要setsid。但是,我没有找到只使用bash习语可靠地创建进程组领导者的方法,抱歉。

这个方法稍微有点复杂,但非常有效,因为它不仅会终止子进程,还会终止子进程在同一进程组中创建的所有内容。

我现在使用以下方法

__git_ps1() { setsid -w /bin/bash -c 'sleep 1 & . /usr/lib/git-core/git-sh-prompt && __git_ps1 "$@" & wait -n; p=$(/usr/bin/ps --no-headers -opgrp $$) && [ $$ = ${p:-x} ] && /usr/bin/kill -9 0; echo "PGRP mismatch $$ $p" >&2' bash "$@"; }

它的功能是什么:
  • setsid -w /bin/bash -c 'SCRIPT' bash "$@" 在一个新的进程组中运行 SCRIPT
  • sleep 1 & 设置超时时间
  • . /usr/lib/git-core/git-sh-prompt && __git_ps1 "$@" & 并行运行 git 提示符
    • /usr/lib/git-core/git-sh-prompt 是针对 Ubuntu 22.04 的,如有需要请更改
  • wait -n; 等待 sleep 或者 __git_ps1 返回
    • 谁先返回就使用谁
  • p=$(/usr/bin/ps --no-headers -opgrp $$) && [ $$ = ${p:-x} ] && 只是一个保护措施,用于检查 setsid 是否成功,并且我们确实是进程组的领导者
    • $$ 在这里正常工作,因为我们在单引号内部
  • kill -9 0 强制杀死整个进程组
    • 所有可能仍在执行的 git 命令
    • 包括 /bin/bash
  • echo "PGRP mismatch $$ $p" >&2' 永远不会执行
    • 这会通知您,要么 setsid 是伪造的
    • 要么其他事情(kill?)没有按预期工作
保护措施是为了防止setsid不按照广告宣传的方式工作。如果没有这个保护措施,你当前的shell可能会被终止,这将导致无法生成一个交互式shell。

如果你使用这个方法并且信任setsid,那么你可能不需要这个保护措施,因此setsid是唯一一个非bash习惯用法所需的。


1
Bash 4 版本非常棒。就这些了。 - system PAUSE
2
这实际上需要 Bash 4.3 或更高版本。cc。'wait'内置有一个新的'-n'选项,以等待下一个子进程改变状态。来自:http://tiswww.case.edu/php/chet/bash/NEWS - Ben Reser
1
在Bash 5.1中,wait命令增加了一个-p参数,我认为这会使得代码更加优雅,因为你可以检测哪个进程先完成,并且可以根据成功或超时的情况进行不同的处理。 - Tom Anderson

18

我更喜欢使用"timelimit",在Debian中至少有一个软件包。

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

它比coreutils中的"timeout"更好用,因为在杀死进程时会输出一些内容,并且默认情况下还会发送SIGKILL信号。


它似乎运行得不太好 :/ $ time timelimit -T2 sleep 10实际时间 0m10.003秒 用户时间 0m0.000秒 系统时间 0m0.000秒 - hithwen
3
请使用 -t2 而不是 -T2。大写的 -T 表示从发送 SIGTERM 信号到发送 SIGKILL 信号之间的时间。 - maxy
1
我想补充一点,timelimit 1.8似乎与fork不兼容("timelimit -t1 ./a_forking_prog" 只会杀死其中一个进程),但timeout可以正常工作。 - Jeremy Cochoy
1
如果你想在杀死进程时打印一些内容,只需使用“-v”标志即可。 - user5739133

9

整洁、简单,使用TERM而不是KILL。太好了!当我最初提出这个问题时,我一直在探索类似于陷阱/等待的解决方案。 - system PAUSE
在 kill 的情况下,超时会返回 124。 - ceving

9

timeout 很可能是第一个尝试的方法。如果超时,您可能需要通知或执行另一个命令。 经过相当多的搜索和实验,我想出了这个 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

1
这是一个优雅的解决方案。易于阅读,易于理解。(但是,如果您想测试所有命令是否成功,它需要使用 && \\ 连接每个命令。) - automorphic

8

有点粗糙,但它能够工作。如果您有其他前台进程,则无法正常工作(请帮助我解决这个问题!)

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秒; kill $ {SPID})&CPID = $ {!}; fg 1; kill $ {CPID})> fdsa - system PAUSE
@system PAUSE,设置-m,我想是这样的。 - strager
我在登录shell中有作业控制(set -m)。那是$-的himBH内容中的“m”,但似乎对于子shell来说会消失。可能是Cygwin的副作用。咕噜声 - system PAUSE
脚本中不要使用“fg”。请阅读“help wait”。 - lhunath
@strager,加1鼓励你尝试,谢谢。你的方法和其他人的方法非常相似,也和我尝试过的一些方法相似。 - system PAUSE

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