如何限制BASH脚本的运行时间

20

我有一个在Windows下通过CYGWIN运行的长时间运行的BASH脚本。

我想将脚本限制在30秒内运行,并在超过此限制时自动终止。理想情况下,我希望能够对任何命令执行此操作。

例如:

sh-3.2$ limittime -t 30 'myscript.sh'

或者
sh-3.2$ limittime -t 30 'grep func *.c'

在cygwin下,ulimit命令似乎无法正常工作。我可以接受任何想法。

@jm,查看我的更新答案以了解如何在超时和子进程正常退出的最早时间停止等待。 - paxdiablo
3
类似的问题,有一些不同的答案:https://dev59.com/f3RB5IYBdhLWcg3wNk93 - system PAUSE
有什么理由不使用GNU的“timeout”实用程序吗? - Chris Johnson
"timeout"很棒!你甚至可以与多条命令(多行脚本)一起使用:https://dev59.com/EloT5IYBdhLWcg3w8jJm#61888916 - Noam Manos
5个回答

19

请查看http://www.pixelbeat.org/scripts/timeout脚本,其功能已经集成到较新的coreutils中:

#!/bin/sh

# Execute a command with a timeout

# License: LGPLv2
# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <brad@footle.org>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <jasan@x31.com>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <jasan@x31.com>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@"& wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value

这是一个不错的脚本。在CYGWIN下也可以正常工作。 - jm.
3
如果你正在使用标准输入重定向,比如:./time_limit.sh cat < my_file.txt那么这个脚本将无法工作。不会输出任何内容。可以通过将"$@"替换为"$@" < /dev/stdin来解决问题。也许有人会发现这很有用。 - siemanko

12
下面的脚本展示了如何使用后台任务。第一部分在10秒限制后杀死60秒的进程。第二部分试图杀死已经退出的进程。请记住,如果您设置了非常高的超时时间,进程ID可能会回滚,您将杀死错误的进程,但这更多是一个理论性问题 - 超时时间必须非常大,并且您必须启动许多进程。
#!/usr/bin/bash

sleep 60 &
pid=$!
sleep 10
kill -9 $pid

sleep 3 &
pid=$!
sleep 10
kill -9 $pid

这是我在Cygwin上得到的输出:

$ ./limit10
./limit10: line 9:  4492 Killed sleep 60
./limit10: line 11: kill: (4560) - No such process

如果你只想等到进程完成,你需要进入一个循环并进行检查。这样做略微不太准确,因为 sleep 1 和其他命令实际上需要超过一秒钟的时间(但不会太多)。使用此脚本替换上面的第二部分("echo $proc" 和 "date" 命令是用于调试的,我不希望在最终解决方案中看到它们)。

#!/usr/bin/bash

date
sleep 3 &
pid=$!
((lim = 10))
while [[ $lim -gt 0 ]] ; do
    sleep 1
    proc=$(ps -ef | awk -v pid=$pid '$2==pid{print}{}')
    echo $proc
    ((lim = lim - 1))
    if [[ -z "$proc" ]] ; then
            ((lim = -9))
    fi
done
date
if [[ $lim -gt -9 ]] ; then
    kill -9 $pid
fi
date

该代码基本上是循环检查进程是否仍在运行。如果没有,它会退出循环并返回一个特殊值以避免尝试杀死子进程。否则,它会在超时后杀死子进程。

以下是输入sleep 3的输出:

Mon Feb  9 11:10:37 WADT 2009
pax 4268 2476 con 11:10:37 /usr/bin/sleep
pax 4268 2476 con 11:10:37 /usr/bin/sleep
Mon Feb  9 11:10:41 WADT 2009
Mon Feb  9 11:10:41 WADT 2009

并且执行 sleep 60 命令:

Mon Feb  9 11:11:51 WADT 2009
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
pax 4176 2600 con 11:11:51 /usr/bin/sleep
Mon Feb  9 11:12:03 WADT 2009
Mon Feb  9 11:12:03 WADT 2009
./limit10: line 20:  4176 Killed sleep 60

这很好,但进程可以运行的最小长度现在是超时时间。虽然从我提出问题的方式来看这是正确的。 - jm.
1
不要使用kill -9,除非绝对必要!SIGKILL无法被捕获,因此被杀死的程序无法运行任何关闭例程,例如擦除临时文件。首先尝试HUP(1),然后是INT(2),然后是QUIT(3)。 - andrewdotn
信号是一个样本,@www。如果你理解底层脚本在做什么,使用-9是相当可接受的。否则,您必须使用HUP,wait,INT,wait,QUIT等来复杂化您的代码。虽然这不是一个好的答案,但是你是对的,在现实世界中可能需要它。 - paxdiablo
1
这种方法需要在后台运行有时间限制的脚本,这可能是不可取的。它还存在'ps'和'kill'之间的竞争条件。我喜欢http://www.bashcookbook.com/bashinfo/source/bash-4.0/examples/scripts/timeout3,它在前台运行,并在KILL之前发送TERM信号。 - system PAUSE
@www.blindrut.ca~neitsch,HUP 对不同的程序意义不同,例如“重新读取配置文件”?我怀疑 TERM 比 INT 更优雅地关闭。https://dev59.com/eXRB5IYBdhLWcg3wNk53 - system PAUSE
很棒的答案,通过谷歌找到并回答了我的(相当不同的)问题。 - Tom Wright

5

看看这个链接。思路就是将myscript.sh作为您的脚本的子进程运行,并记录其PID,如果运行时间过长,则终止它。


我无法在cygwin上运行那个示例。我得到了以下错误: sh: line 46: kill: SIGUSR1: invalid signal specification - jm.
这个解决方案看起来有些奇怪。它启动定时任务,然后在一个单独的子shell中休眠并向正在运行的shell发送USR1信号。为什么不直接在正在运行的shell中休眠呢? - paxdiablo
就像Pax所接受的答案一样,该脚本要求您的脚本在后台运行,并且在“ps”和“kill”之间存在竞争条件。当然,这种竞争条件严格来说只是一个表面问题。 - system PAUSE

5
timeout 30s YOUR_COMMAND COMMAND_ARGUMENTS

以下是coreutils中“timeout”的所有选项:
$ timeout --help
Usage: timeout [OPTION] DURATION COMMAND [ARG]...
  or:  timeout [OPTION]
Start COMMAND, and kill it if still running after DURATION.

Mandatory arguments to long options are mandatory for short options too.
      --preserve-status
                 exit with the same status as COMMAND, even when the
                   command times out
      --foreground
                 when not running timeout directly from a shell prompt,
                   allow COMMAND to read from the TTY and get TTY signals;
                   in this mode, children of COMMAND will not be timed out
  -k, --kill-after=DURATION
                 also send a KILL signal if COMMAND is still running
                   this long after the initial signal was sent
  -s, --signal=SIGNAL
                 specify the signal to be sent on timeout;
                   SIGNAL may be a name like 'HUP' or a number;
                   see 'kill -l' for a list of signals
      --help     display this help and exit
      --version  output version information and exit

DURATION is a floating point number with an optional suffix:
's' for seconds (the default), 'm' for minutes, 'h' for hours or 'd' for days.

If the command times out, and --preserve-status is not set, then exit with
status 124.  Otherwise, exit with the status of COMMAND.  If no signal
is specified, send the TERM signal upon timeout.  The TERM signal kills
any process that does not block or catch that signal.  It may be necessary
to use the KILL (9) signal, since this signal cannot be caught, in which
case the exit status is 128+9 rather than 124.

GNU coreutils online help: <http://www.gnu.org/software/coreutils/>
Full documentation at: <http://www.gnu.org/software/coreutils/timeout>
or available locally via: info '(coreutils) timeout invocation'

2

您可以将命令作为后台任务运行(即使用"&"),使用bash变量获取“上次运行命令的pid”,等待所需时间,然后使用该pid运行kill命令。


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