在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个回答

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
$

4
如果你已经知道需要在超时时间(比如3秒)后终止程序的名称(假设为program),我可以提供一种简单而不太优雅的替代方案:
(sleep 3 && killall program) & ./program

如果我使用系统调用来调用基准测试进程,这将完美地运作。

3
这会终止使用该名称的其他进程,但不会终止更改其名称的具有给定名称的进程(例如,通过写入argv [0],可能使用其他技巧来腾出更多空间)。 - Jed
当我尝试在一定时间后停止docker容器时,我发现这个小技巧非常有用。Docker客户端似乎无法以实际停止前台运行的容器的方式接受TERM/INT/KILL。因此,给容器命名并使用(sleep 3 && docker stop <container>) &效果很好。谢谢! - Mat Schaffer
是的,这段代码很简陋且有限,但可以进行改进,例如: { sleep 5 && kill -9 $(ps -fe | grep "program" | grep $$ | tr -s " " | cut -d" " -f2); } & SLEEPPID=$!; bash -c "program" && kill -9 $SLEEPPID" 这样,它只会杀死当前 shell 中的作业。 - ton

2

目前,OS X还没有使用bash 4版本,也没有/usr/bin/timeout,因此这里提供一种函数,在不需要home-brew或macports的情况下,可以在OS X上使用,并类似于/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
) }

2

还有由Martin Cracauer编写的cratimeout(用于Unix和Linux系统的C语言编写)。

# 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'

特别是,它保留了信号行为。有这个选项真不错! - system PAUSE

1
如果您想在脚本中完成此操作,请将以下内容放入其中:
parent=$$
( sleep 5 && kill -HUP $parent ) 2>/dev/null &

你能解释一下这段代码在做什么吗? - Joshua Pinter
1
一个子进程等待5秒钟,然后向父进程发送挂断信号。 - nroose
完美的解释,谢谢! - Joshua Pinter

1
超时命令本身具有一个--foreground选项。这使得命令可以与用户交互,“当不直接从shell提示符运行超时时”。
timeout --foreground the_command its_options

我认为提问者一定知道timeout命令的显而易见的解决方案,但由于某些原因寻求另一种解决方案。当我使用popen调用timeout时,即“不直接从shell中调用”时,timeout对我无效。然而,我不能假设这可能是提问者的情况。请查看其man page

1
使用 --foreground 选项,Ctrl-C(SIGINT)将起作用,但不会杀死任何子进程。 - Pablo Bianchi
1
“我认为提问者必须已经知道了timeout命令的非常明显的解决方案” - 不,当我14年前提出这个问题时,我并不知道。从其他答案和评论中可以看出,timeout并不是到处都可用的,至少不是默认情况下。但感谢您提到--foreground选项。 - system PAUSE

1
这是一个不依赖于生成子进程的版本 - 我需要一个独立的脚本来嵌入此功能。它还可以进行分数轮询间隔,因此您可以更快地轮询。虽然 timeout 更好,但我卡在了一台旧服务器上。
# 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
谢谢!这是这里最好的解决方案,在MacOS上完美运行,不依赖于bash(它在/bin/sh上也可以正常运行),而且非常精确!非常感谢您提供这样的代码! - Prado

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

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

我的问题可能有点不同:我通过ssh在远程机器上启动一个命令,并希望在命令挂起时杀死shell和子进程。

我现在使用以下命令:

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一样运行


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