自动终止某个命令的命令行命令在一定时间后。

60

我想要在特定时间后自动杀掉一个命令。我考虑的界面如下:

% constrain 300 ./foo args

有一个问题,如何在5分钟后自动杀掉"./foo"进程,但如果该进程占用过多内存也会自动杀掉。

是否有现成的工具或者有人编写过这样的工具?

补充:Jonathan的解决方案完全符合我所想,并且在linux上运行良好,但是我无法使其在Mac OSX上运行。我去掉了SIGRTMIN,可以正常编译,但信号没有被发送到子进程中。有人知道如何在Mac上实现这个功能吗?

[添加:来自Jonathan的更新版可在Mac和其他系统上使用]


你的问题是什么? - Daren Thomas
是的,真的。您在这里并没有提出问题。 - mmcdole
1
类似的问题,有一些不同的答案:stackoverflow.com/questions/687948 - system PAUSE
1
链接到 http://unix.stackexchange.com/questions/21542/from-shell-killing-a-process-if-wallclock-runtime-exceeds-some-predetermined-va/21544#comment111350_21544 - Etienne Low-Décarie
可能是如何在Bash中给定超时后终止子进程?的重复问题。虽然它要求对问题主体进行概括,但“超时”已经成为主导因素,我认为最好针对每个问题提出一个具体的概括。 - Ciro Santilli OurBigBook.com
15个回答

52

13
太好了!因为GNU Coreutils可以通过Homebrew获得,所以只需运行brew install coreutils命令,然后就可以使用gtimeout了。 - Stefan Schmidt
@talyric:默认情况下,OS X 不提供 GNU Coreutils。请参阅如何用 GNU 核心实用程序替换 Mac OS X 实用程序? - Roger Dahl

41

也许我没有理解问题,但是在bash中,这似乎可以直接完成:

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

这会在括号内运行第一个命令5秒钟,然后杀死它。整个操作是同步运行的,也就是说,在等待缓慢的命令时,你将无法使用你的Shell。如果这不是您想要的,可以尝试添加另一个 &。

$!变量是Bash内置变量,包含最近启动的子Shell进程的进程ID。重要的是不要在括号内使用 &,这样做会丢失进程ID。


非常聪明!我不知道Bash可以做到这一点。有人知道tcsh的类似技巧吗? - dreeves
我在 https://dev59.com/f3RB5IYBdhLWcg3wNk93 上对你的聪明回答进行了追问。 - system PAUSE
6
非常有趣,但如果进程提前终止并且PID被重新用于同一用户的另一个不相关进程,则存在竞态条件。现在我必须阅读系统PAUSE的后续答案,并查看它们是否有相同的问题。 - Pascal Cuoq

30

我比较晚才参加这个聚会,但我没有看到我的最爱技巧在答案中列出。

在 *NIX 下,alarm(2) 命令在 execve(2) 命令之间被继承,而 SIGALRM 默认是致命的。因此,你通常可以简单地执行以下操作:

$ doalarm () { perl -e 'alarm shift; exec @ARGV' "$@"; } # define a helper function

$ doalarm 300 ./foo.sh args

或者安装一个微不足道的C包装器来代替你完成这个任务。

优点 只涉及一个PID,机制简单。如果例如./foo.sh退出“太快”,其PID被重新使用,你就不会误杀进程。你不需要几个shell子进程协同工作,虽然可以正确执行但容易出现竞争条件。

缺点 时间受限的进程无法操作它的闹钟(例如alarm(2)ualarm(2)setitimer(2)),因为这可能会清除继承的闹钟。显然,对于其他一些方法而言,它也不能阻塞或忽略SIGALRM,尽管对于SIGINT、SIGTERM等信号也是如此。

一些(非常老旧的)系统用alarm(2)实现sleep(2),即使今天有些程序员也将alarm(2)用作I/O和其他操作的粗略内部超时机制。但根据我的经验,这种技术适用于你想要限制时间的绝大多数进程。


我一直在使用Perl的单行脚本。我遇到了一个奇怪的问题:当命令是Bash函数时,它似乎无法正常工作。doalarm 7200 echo "Cool"可以完美运行,testfun () { echo $1 } ; testfun "Cool"也可以完美运行,但是doalarm 7200 testfun "Cool"却无法正常工作。感谢任何建议。 - Etienne Low-Décarie
1
@EtienneLow-Décarie,是的,这种包装器的风格只适用于作为单独进程执行的命令。因此,shell函数和内置命令将无法进行doalarm操作。 - pilcrow
感谢@pilcrow!这是否产生一个变量,根据进程在超时后是否被杀死或进程是否完成而保存一个值? - Etienne Low-Décarie
1
@EtienneLow-Décarie,简单来说,shell中的$?将编码退出状态。 信号死亡是$?&127,例如,在我的系统上,由SIGALRM导致的死亡是14。 普通退出,成功或失败,是$? >> 8,假设没有信号死亡。 - pilcrow
做得好!也许更实用的退出码行为总结是:如果命令超时,退出码将设置为142128 + 1414是标准化的SIGALRM信号编号);否则,命令自己的退出码将被传递。 - mklement0
显示剩余2条评论

13

我有一个程序叫做timeout,可以实现这个功能 - 它是用C编写的,最初是在1989年编写的,但自那以后定期更新。



/*
@(#)File:           $RCSfile: timeout.c,v $
@(#)Version:        $Revision: 4.6 $
@(#)Last changed:   $Date: 2007/03/01 22:23:02 $
@(#)Purpose:        Run command with timeout monitor
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1989,1997,2003,2005-07
*/

#define _POSIX_SOURCE       /* Enable kill() in <unistd.h> on Solaris 7 */
#define _XOPEN_SOURCE 500

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "stderr.h"

#define CHILD       0
#define FORKFAIL    -1

static const char usestr[] = "[-vV] -t time [-s signal] cmd [arg ...]";

#ifndef lint
/* Prevent over-aggressive optimizers from eliminating ID string */
const char jlss_id_timeout_c[] = "@(#)$Id: timeout.c,v 4.6 2007/03/01 22:23:02 jleffler Exp $";
#endif /* lint */

static void catcher(int signum)
{
    return;
}

int main(int argc, char **argv)
{
    pid_t   pid;
    int     tm_out;
    int     kill_signal;
    pid_t   corpse;
    int     status;
    int     opt;
    int     vflag = 0;

    err_setarg0(argv[0]);

    opterr = 0;
    tm_out = 0;
    kill_signal = SIGTERM;
    while ((opt = getopt(argc, argv, "vVt:s:")) != -1)
    {
        switch(opt)
        {
        case 'V':
            err_version("TIMEOUT", &"@(#)$Revision: 4.6 $ ($Date: 2007/03/01 22:23:02 $)"[4]);
            break;
        case 's':
            kill_signal = atoi(optarg);
            if (kill_signal <= 0 || kill_signal >= SIGRTMIN)
                err_error("signal number must be between 1 and %d\n", SIGRTMIN - 1);
            break;
        case 't':
            tm_out = atoi(optarg);
            if (tm_out <= 0)
                err_error("time must be greater than zero (%s)\n", optarg);
            break;
        case 'v':
            vflag = 1;
            break;
        default:
            err_usage(usestr);
            break;
        }
    }

    if (optind >= argc || tm_out == 0)
        err_usage(usestr);

    if ((pid = fork()) == FORKFAIL)
        err_syserr("failed to fork\n");
    else if (pid == CHILD)
    {
        execvp(argv[optind], &argv[optind]);
        err_syserr("failed to exec command %s\n", argv[optind]);
    }

    /* Must be parent -- wait for child to die */
    if (vflag)
        err_remark("time %d, signal %d, child PID %u\n", tm_out, kill_signal, (unsigned)pid);
    signal(SIGALRM, catcher);
    alarm((unsigned int)tm_out);
    while ((corpse = wait(&status)) != pid && errno != ECHILD)
    {
        if (errno == EINTR)
        {
            /* Timed out -- kill child */
            if (vflag)
                err_remark("timed out - send signal %d to process %d\n", (int)kill_signal, (int)pid);
            if (kill(pid, kill_signal) != 0)
                err_syserr("sending signal %d to PID %d - ", kill_signal, pid);
            corpse = wait(&status);
            break;
        }
    }

    alarm(0);
    if (vflag)
    {
        if (corpse == (pid_t) -1)
            err_syserr("no valid PID from waiting - ");
        else
            err_remark("child PID %u status 0x%04X\n", (unsigned)corpse, (unsigned)status);
    }

    if (corpse != pid)
        status = 2; /* I don't know what happened! */
    else if (WIFEXITED(status))
        status = WEXITSTATUS(status);
    else if (WIFSIGNALED(status))
        status = WTERMSIG(status);
    else
        status = 2; /* I don't know what happened! */

    return(status);
}

如果您需要“stderr.h”和“stderr.c”的“官方”代码,请联系我(请参阅我的个人资料)。

Jonathan,非常感谢!这在Linux上运行得很好,但在我的Mac OSX上无法运行。它编译(删除SIGRTMIN后)并似乎正在工作,但实际上没有发送信号。 - dreeves
2
对于那些过于害羞联系Jonathan的人,他说我可以放置代码,所以这里是链接:http://yootles.com/outbox/timeout-4.09.tgz 感谢Jonathan提供此方便!(他还表示:“顺便提一下,我认为Cygwin也需要sigaction()修复。”) - dreeves

13

还有一个 ulimit 命令,可以用于限制子进程可用的执行时间。

ulimit -t 10

限制进程的CPU时间为10秒。

如果要将其用于限制新进程而不是当前进程,您可能希望使用一个包装脚本:

#! /usr/bin/env python

import os
os.system("ulimit -t 10; other-command-here")

other-command 可以是任何工具。我运行了不同排序算法的 Java、Python、C 和 Scheme 版本,记录它们执行所需的时间,并将执行时间限制为 30 秒。一个 Cocoa-Python 应用程序生成了各种命令行(包括参数)并将时间整合到 CSV 文件中,但事实上它只是在上面添加了一些额外信息。


啊哈,我忘记了ulimit。但是有没有办法将其应用于特定的进程?例如,可以使用ulimit更简单地完成Jonathan所做的事情吗? - dreeves
我已经很久没有使用ulimit了。我的做法是运行一个Python脚本,该脚本运行另一个工具:os.system("ulimit -t 600; gtime -f <command> java -Xms1024m -Xmx1024m Main")。 - Matthew Schinckel
谢谢Matthew!想把这个加入你的答案里吗?看起来这是Jonathan C程序的一个简单而快速的替代方法。甚至可以将其包装在perl / python / shell脚本中,并具有相同的命令行界面。与Jonathan的解决方案相比,这样做的优缺点是什么呢? - dreeves

4
#!/bin/sh
( some_slow_task ) & pid=$!
( sleep $TIMEOUT && kill -HUP $pid ) 2>/dev/null & watcher=$!
wait $pid 2>/dev/null && pkill -HUP -P $watcher

观察者在给定的超时时间后终止缓慢的任务;脚本等待缓慢的任务并终止观察者。

示例:

  • 缓慢的任务运行超过2秒钟并被终止

缓慢的任务中断

( sleep 20 ) & pid=$!
( sleep 2 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "Slow task finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "Slow task interrupted"
fi
  • 在给定的超时时间内,此慢速任务已完成。

慢速任务已完成

( sleep 2 ) & pid=$!
( sleep 20 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "Slow task finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "Slow task interrupted"
fi

这比 Perl 版本更好,因为 SIGALARM 是否起作用是未知的。 - user239558

4

4

我的这个perl one-liner的变体可以让你获取退出状态,而不需要涉及fork()和wait(),也不会有误杀进程的风险:

#!/bin/sh
# Usage: timelimit.sh secs cmd [ arg ... ]
exec perl -MPOSIX -e '$SIG{ALRM} = sub { print "timeout: @ARGV\n"; kill(SIGTERM, -$$); }; alarm shift; $exit = system @ARGV; exit(WIFEXITED($exit) ? WEXITSTATUS($exit) : WTERMSIG($exit));' "$@"

基本上,fork()和wait()被隐藏在system()内部。SIGALRM会传递给父进程,然后通过向整个进程组(-$$)发送SIGTERM来杀死自己和子进程。如果子进程退出并且在kill()发生之前重用了子进程的pid,则这不会杀死错误的进程,因为具有旧子进程pid的新进程不会在父perl进程的同一进程组中。
作为额外的好处,脚本还以可能是正确的退出状态退出。

我有一个问题想问你。当我在Mac OS上通过bash调用timelimit.sh secs cmd [args]时,它按预期工作并在5秒后杀死由cmd [args]启动的进程。但是,当我通过Python使用os.system或subprocess模块调用timelimit.sh时,它无法杀死由cmd [args]启动的进程。你有什么想法为什么会这样? - user2635911

4

Perl一行代码,只是为了好玩:

perl -e '$s = shift; $SIG{ALRM} = sub { print STDERR "Timeout!\n"; kill INT => $p }; exec(@ARGV) unless $p = fork; alarm $s; waitpid $p, 0' 10 yes foo

这会打印出'foo'十秒钟,然后超时。将'10'替换为任何秒数,并将'yes foo'替换为任何命令。


不错!我们现在有三种方法可以做到这一点:Jonathan的C程序,Matthew的ulimit技巧以及这个Perl脚本(在概念上类似于Jonathan的程序)。对于不同解决方案的优缺点,您有什么想法吗? - dreeves
嗯,ulimit不同。我认为它限制的是CPU时间而不是墙钟时间,这可能很奇怪:如果你卡住了双核盒子的两个处理器会发生什么?或者如果你睡觉并放弃CPU时间呢?乔纳森的程序和我的类似。他的退出状态正确,我的不需要编译器。 - vasi

2
使用 expect 工具怎么样?
## run a command, aborting if timeout exceeded, e.g. timed-run 20 CMD ARGS ...
timed-run() {
  # timeout in seconds
  local tmout="$1"
  shift
  env CMD_TIMEOUT="$tmout" expect -f - "$@" <<"EOF"
# expect script follows
eval spawn -noecho $argv
set timeout $env(CMD_TIMEOUT)
expect {
   timeout {
      send_error "error: operation timed out\n"
      exit 1
   }
   eof
}
EOF
}

这感觉有点过度设计,但仍然是非常有用的答案,特别是作为更复杂的模板,在真正需要Expect的地方。谢谢! - dreeves

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