在 bash 中超时命令,不要有不必要的延迟

这个回答 到 < a href = “ https://stackoverflow./questions/601543”> 命令行命令在一定时间后自动终止命令

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

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

但是,给定的“长时间运行”命令可能比超时更早完成。
(让我们称之为“典型的-长时间运行但有时很快”命令,或者为了好玩而称之为 Tlrbsf。)

因此,这种漂亮的一行程序方法存在一些问题。
首先,sleep不是条件的,因此它对序列完成所需的时间设置了一个不理想的下限。当 Tlrbsf命令在2秒钟内完成时,可以考虑睡眠时间为30秒、2米甚至5米,这是非常不可取的。
其次,kill是无条件的,因此这个序列将尝试终止一个非运行进程,并对它发牢骚。

那么..。

有没有一种 超时的方法,使一个典型的长时间运行但有时很快的(“ tlrbsf”)命令

  • 有一个 bash 实现(另一个问题已经有了 Perl 和 C 的答案)
  • 将在两个程序中较早的时候终止: Tlrbsf程序终止,或超时过去
  • 不会杀死不存在/不运行的进程(或者,可选地: 不会杀死坏进程的 抱怨)
  • 不一定非得是一次性手术
  • 可以在 Cygwin 或 Linux 下运行

还有额外加分

  • 运行前台的 Tlrbsf命令
  • 任何“睡眠”或后台的额外过程

使得 Tlrbsf命令的 stdin/stdout/stderr 可以被重定向,就像它被直接运行一样?

如果有,请分享你的代码。如果没有,请解释原因。

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

404843 次浏览

有点俗气,但很管用。不工作,如果你有其他前台进程(请帮我修复这个!)

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

我认为这正是你所要求的:

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 "$@"

在99%的情况下,答案是不实现任何超时逻辑。超时逻辑在几乎任何情况下都是一个红色警告信号,表明其他的有错误,应该修复而不是

有时你的进程会在n秒后挂起或中断吗?然后找出原因并解决这个问题。

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

另请参阅http://www.pixelbeat.org/scripts/timeout脚本,其功能已集成到更新的coreutils中

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

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

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

你可能在coreutils中寻找timeout命令。因为它是coreutils的一部分,所以技术上它是一个C解决方案,但它仍然是coreutils。info timeout获取更多详细信息。 下面是一个例子:

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

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

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

如果我用系统调用来调用基准测试进程,这将非常有效。

该解决方案不考虑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不适用于不同shell的子进程。

例子:

  • 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
#! /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 "$@"

我提出了一个问题,以保留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的目录

还有Martin Cracauer编写的cratimeout(用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'

一个非常简单的方法:

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

使用pkill(选项- f),你可以用参数终止特定的命令,或者指定-n来避免终止旧进程。

你完全可以用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

  • wait -n需要Bash 4.3

  • 如果命令被杀死,则返回137,否则返回命令的返回值。
  • 适用于管道。(你不需要去前台这里!)
  • 也适用于内部shell命令或函数。
  • 在子shell中运行,所以没有变量导出到当前shell,抱歉。

如果你不需要返回代码,这可以变得更简单:

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

注:

  • 严格地说,在; )中不需要;,但是它使事情与__abc2情况更加一致。set +b可能也可以被丢弃,但小心总比后悔好。

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

这个recipe可以在shell中“自然地”使用(就像flock fd一样自然):

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

但是,如上所述,不能自然地以这种方式将环境变量重新导出到外壳中。

编辑:

真实世界的例子:超时__git_ps1,以防它花费太长时间(对于像缓慢的SSHFS-Links这样的事情):

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

Edit2:错误修复。我注意到exit 137是不需要的,同时使得_timeout不可靠。

Edit3: git是一个顽固分子,所以它需要双重技巧才能令人满意地工作。

Edit4:在真实的GIT示例中,第一个_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

OS X还没有使用bash 4,也没有/usr/bin/timeout,所以这里有一个在OS X上工作的函数,没有自制或macports,类似于/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
) }

我的问题可能有点不同:我在远程机器上通过ssh启动一个命令,如果命令挂起,我想杀死shell和子程序。

我现在使用以下方法:

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

这样,当有超时时,命令返回255,如果成功,则返回命令的returncode

请注意,从ssh会话中杀死进程的处理方式与交互式shell不同。但是您也可以使用-t选项为ssh分配一个伪终端,这样它就像一个交互式shell

就是这样:

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

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

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

# 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

简单的脚本,代码清晰。保存到/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
$

@loup的回答上构建…

如果你想让一个进程超时并关闭kill job/pid输出,执行以下命令:

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

这将后台进程放入一个子shell中,因此您看不到作业输出。

我有一个cron工作,调用一个php脚本,有时,它会卡在php脚本。这个解决方案对我来说是完美的。

我使用:

scripttimeout -t 60 /script.php

在1秒后使slowcommand超时:

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

要确定命令是否超时或由于自身原因失败,请检查状态代码是否为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。不过,在这两种情况下,您都可以安全地假设发生了某种超时。

timeout命令本身有一个--foreground选项。这让命令在没有直接从shell提示符超时时与用户交互。

timeout --foreground the_command its_options

我认为提问者一定已经意识到timeout命令的非常明显的解决方案,但出于这个原因要求另一种解决方案。当我使用popen调用它时,timeout没有为我工作。“不是直接从壳里取的。”然而,我不认为这就是提问者的原因。看看它的手册页

如果你想在你的脚本中这样做,把这个放在那里:

parent=$$
( sleep 5 && kill -HUP $parent ) 2>/dev/null &