为bash命令和函数实现超时的优雅解决方案

时间:2014-06-25 15:25:01

标签: bash

我编写了一个运行命令的函数,它以两个args为第一个命令,以秒为单位的第二个超时:

#! /bin/bash

function run_cmd {
    cmd="$1"; timeout="$2"
    grep -qP "^\d+$" <<< "$timeout" || timeout=10

    stderrfile=$(readlink /proc/$$/fd/2)
    exec 2<&-

    exitfile=/tmp/exit_$(date +%s.%N)
    (eval "$cmd";echo $? > $exitfile) &

    start=$(date +%s)
    while true; do
        pid=$(jobs -l | awk '/Running/{print $2}')
        if [ -n "$pid" ]; then
            now=$(date +%s)
            running=$(($now - $start))
            if [ "$running" -ge "$timeout" ];then
                kill -15 "$pid"
                exit=1
            fi
            sleep 1
        else
            break
        fi

    done 
    test -n "$exit" || exit=$(cat $exitfile)
    rm $exitfile
    exec 2>$stderrfile              
    return "$exit"
}


function sleep5 {
    sleep 5
    echo "I slept 5"
    return 2
}

run_cmd sleep5 "6" 
run_cmd sleep5 "3"
echo "hi" >&2 

该功能正常但我不确定这是一个优雅的解决方案,我想知道以下的替代方案

  1. 我必须将退出状态存储在文件中:(eval "$cmd";echo $? > $exitfile)
  2. 我正在关闭并重新开启STDERR:exec 2<&- and exec 2>$stderrfile
  3. 我正在关闭STDERR,因为在杀死命令时我无法避免该消息:

    test.sh: line 3: 32323 Terminated ( eval "$cmd"; echo $? > $exitfile )

    PS:我知道timeoutexpect,但它们无法用于功能。

3 个答案:

答案 0 :(得分:9)

也许这符合您的需求。我更改了呼叫签名,以避免使用eval

# Usage: run_with_timeout N cmd args...
#    or: run_with_timeout cmd args...
# In the second case, cmd cannot be a number and the timeout will be 10 seconds.
run_with_timeout () { 
    local time=10
    if [[ $1 =~ ^[0-9]+$ ]]; then time=$1; shift; fi
    # Run in a subshell to avoid job control messages
    ( "$@" &
      child=$!
      # Avoid default notification in non-interactive shell for SIGTERM
      trap -- "" SIGTERM
      ( sleep $time
        kill $child 2> /dev/null ) &
      wait $child
    )
}

示例,显示退出状态:

$ sleep_and_exit() { sleep ${1:-1}; exit ${2:-0}; }

$ time run_with_timeout 1 sleep_and_exit 3 0; echo $?

real    0m1.007s
user    0m0.003s
sys     0m0.006s
143

$ time run_with_timeout 3 sleep_and_exit 1 0; echo $?

real    0m1.007s
user    0m0.003s
sys     0m0.008s
0

$ time run_with_timeout 3 sleep_and_exit 1 7; echo $?

real    0m1.006s
user    0m0.001s
sys     0m0.006s
7

如图所示,run_with_timeout的退出状态将是执行命令的退出状态,除非它被超时杀死,在这种情况下它将是143(128 + 15)。

注意:如果你设置了一个大的超时和/或运行了一个forkbomb,你可能会以足够快的速度回收pid,kill-child会杀死错误的进程。

答案 1 :(得分:1)

如果要控制函数,可以使用陷阱处理程序(如C语言)

$ trap 'break' 15
$ echo $$; while :; do :; done; echo 'endlessloop terminated'
5168
endlessloop terminated
$

如果在另一个shell中键入kill -15 5168,程序将中断并打印endlessloop terminated

如果您正在产生子流程,请注意另外四件事

  1. 如果子进程在睡眠前很久结束,则会导致长睡眠过程。 因此,最好保持睡眠时间短,并继续多次检查。例如,最好进行10次睡眠,而不是睡眠3600次= 1小时。因为睡眠可能会使您的进程表填满限制。 (或者你必须在$ cmd完成后立即杀死睡眠。)

  2. 如果该过程对正常杀戮没有反应,您可能希望在几秒钟之后再添加kill -9

  3. 如果您需要进程的返回值,则必须使用包装器扩展程序,该包装器将返回值传递给文件/ fifo。

  4. 如果你需要进程的stdout / stderr输出,... file / fifo。

  5. C程序时间限制涵盖了所有这些内容。

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

    $ timelimit
    timelimit: using defaults: warntime=3600, warnsig=15, killtime=120, killsig=9
    timelimit: usage: timelimit [-pq] [-S ksig] [-s wsig] [-T ktime] [-t wtime] command
    

    这个计划有一些好处:

    • 检查,如果进程仍在运行且在休眠时没有退出
    • 如果首先发送一个软killsignal,如果不起作用,则发送一个硬-9信号
    • 它传播(选项-p)返回级别($?),以便您可以将它用于您的目的。

答案 2 :(得分:0)

我相信我有一个基于@rici答案(我接受)的优雅解决方案,并决定我将分享最终结果,我还添加了一个重试功能,这是真正的目标。

function run_cmd { 
    cmd="$1"; timeout="$2";
    grep -qP '^\d+$' <<< $timeout || timeout=10

    ( 
        eval "$cmd" &
        child=$!
        trap -- "" SIGTERM 
        (       
                sleep $timeout
                kill $child 
        ) > /dev/null 2>&1 &     
        wait $child
    )
}

function retry { 
        cmd=$1; timeout=$2; tries=$3; interval=$4
        grep -qP '^\d+$' <<< $timeout || timeout=10
        grep -qP '^\d+$' <<< $tries || tries=3 
        grep -qP '^\d+$' <<< $interval || interval=3
        for ((c=1; c <= $tries; c++)); do
                run_cmd "$cmd" "$timeout" && return
                sleep $interval
        done    
        return 1
}

重试功能接受4个参数:

  1. 命令
  2. 超时
  3. 尝试
  4. 间隔
  5. 可以按如下方式执行:

    retry "some_command_or_function arg1 arg2 .." 5 2 10