【问题标题】:Command line command to auto-kill a command after a certain amount of time命令行命令在一定时间后自动终止命令
【发布时间】:2010-10-10 17:37:57
【问题描述】:

我想在一定时间后自动终止命令。我想到了这样的界面:

% constrain 300 ./foo args

它会运行带有“args”的“./foo”,但如果它在 5 分钟后仍在运行,则会自动终止它。

将这个想法推广到其他约束可能很有用,例如如果进程使用过多内存则自动终止进程。

是否有任何现有的工具可以做到这一点,或者有没有人写过这样的东西?

添加:乔纳森的解决方案正是我的想法,它在 linux 上就像一个魅力,但我无法让它在 Mac OSX 上工作。我摆脱了 SIGRTMIN 让它编译得很好,但信号只是没有被发送到子进程。有人知道如何在 Mac 上进行这项工作吗?

[添加:请注意,Jonathan 提供了适用于 Mac 和其他地方的更新。]

【问题讨论】:

标签: macos unix command-line utilities cpu-usage


【解决方案1】:

我有一个名为 timeout 的程序可以做到这一点 - 用 C 语言编写,最初是在 1989 年,但此后定期更新。


更新:此代码无法在 MacOS X 上编译,因为未定义 SIGRTMIN,并且在 MacOS X 上运行时无法超时,因为那里的 `signal()` 函数在警报超时后恢复 `wait()` - 这不是所需的行为。我有一个新版本的 `timeout.c` 可以解决这两个问题(使用 `sigaction()` 而不是 `signal()`)。和以前一样,请与我联系以获取 10K gzipped tar 文件,其中包含源代码和手册页(请参阅我的个人资料)。
/*
@(#)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”的“官方”代码,请联系我(查看我的个人资料)。

【讨论】:

  • 乔纳森,非常感谢!这在 linux 上工作得很好,但对我来说在 Mac OSX 上不起作用。它编译(在删除 SIGRTMIN 之后)并且似乎正在工作,但实际上并没有发送信号。
  • 对于那些不敢联系乔纳森的人来说,他说欢迎我来托管代码,所以这里是:yootles.com/outbox/timeout-4.09.tgz 再次感谢乔纳森,让这个代码可用! (也来自他:“顺便说一下,我认为 Cygwin 也需要 sigaction() 修复。”)
【解决方案2】:

GNU Coreutils 包含 timeout 命令,默认安装在许多系统上。

https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html

观看free -m 一分钟,然后通过发送 TERM 信号将其杀死:

timeout 1m watch free -m

【讨论】:

【解决方案3】:

我参加这个聚会很晚,但我没有在答案中看到我最喜欢的把戏。

在 *NIX 下,alarm(2) 继承于 execve(2),默认情况下 SIGALRM 是致命的。因此,您通常可以简单地:

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

$ doalarm 300 ./foo.sh args

或者安装一个trivial C wrapper 来为你做这件事。

优势 只涉及一个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" 不起作用。感谢您的任何建议。
  • @EtienneLow-Décarie,是的,这种风格的包装器仅适用于 exec()d 作为单独进程的命令。因此,shell 函数和内置函数将无法报警。
  • 谢谢@pilcrow!这是否会产生一个变量,该变量的值取决于进程是在超时后被终止还是进程完成?
  • @EtienneLow-Décarie,简短的回答是 shell 中的 $? 将对退出状态进行编码。信号死亡是$? &amp; 127,例如,在我的系统上是 14,因为 SIGALRM 死亡。普通退出,成功或失败,是$? &gt;&gt; 8 假设没有信号死亡。
【解决方案4】:

有没有办法用“at”设置一个特定的时间来做到这一点?

$ at 05:00 PM kill -9 $pid

看起来简单多了。

如果您不知道 pid 号是多少,我认为有一种方法可以使用 ps aux 和 grep 编写脚本读取它,但不确定如何实现。

$   | grep someprogram
tony     11585  0.0  0.0   3116   720 pts/1    S+   11:39   0:00 grep someprogram
tony     22532  0.0  0.9  27344 14136 ?        S    Aug25   1:23 someprogram

您的脚本必须读取 pid 并为其分配一个变量。 我不是太熟练,但假设这是可行的。

【讨论】:

    【解决方案5】:
    #!/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 是否有效,它都是命中注定的。
    【解决方案6】:

    我对 perl 单行的变体为您提供了退出状态,而无需使用 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 发送到整个进程组 (-$$) 来杀死它自己和它的子进程。万一子进程退出并且子进程的 pid 在 kill() 发生之前被重用,这不会杀死错误的进程,因为具有旧子进程 pid 的新进程不会在父 perl 进程的同一进程组中.

    作为一个额外的好处,脚本也会以可能正确的退出状态退出。

    【讨论】:

    • 我有一个问题要问你。当我在 mac os 上通过 bash 调用 timelimit.sh secs cmd [args] 时,它会按预期工作并杀死由 cmd [args] 启动的进程,如果它们在 5 秒后仍然存在。但是,当我使用命令 os.system 或 subprocess 模块通过 Python 调用 timelimit.sh 时,它不会终止由 cmd [args] 启动的进程。知道为什么吗?
    【解决方案7】:

    从源代码编译以在 Mac 上工作时来自 Ubuntu/Debian 的超时命令。达尔文

    10.4.*

    http://packages.ubuntu.com/lucid/timeout

    【讨论】:

      【解决方案8】:

      我使用“timelimit”,它是 debian 存储库中提供的一个包。

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

      【讨论】:

        【解决方案9】:

        纯 bash:

        
        #!/bin/bash
        
        if [[ $# < 2 ]]; then
          echo "Usage: $0 timeout cmd [options]"
          exit 1
        fi
        
        TIMEOUT="$1"
        shift
        
        BOSSPID=$$
        
        (
          sleep $TIMEOUT
          kill -9 -$BOSSPID
        )&
        TIMERPID=$!
        
        trap "kill -9 $TIMERPID" EXIT
        
        eval "$@"
        

        【讨论】:

          【解决方案10】:

          使用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 的更复杂的东西的模板。谢谢!
          【解决方案11】:

          尝试类似:

          # This function is called with a timeout (in seconds) and a pid.
          # After the timeout expires, if the process still exists, it attempts
          # to kill it.
          function timeout() {
              sleep $1
              # kill -0 tests whether the process exists
              if kill -0 $2 > /dev/null 2>&1 ; then
                  echo "killing process $2"
                  kill $2 > /dev/null 2>&1
              else
                  echo "process $2 already completed"
              fi
          }
          
          <your command> &
          cpid=$!
          timeout 3 $cpid
          wait $cpid > /dev/null 2>&
          exit $?
          

          它的缺点是如果您的进程的 pid 在超时内被重用,它可能会杀死错误的进程。这不太可能,但您可能每秒启动 20000 多个进程。这可以修复。

          【讨论】:

            【解决方案12】:

            对 perl 单行稍作修改将获得正确的退出状态。

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

            基本上exit($?>>8)会转发子进程的退出状态。我只是在退出状态选择了77超时。

            【讨论】:

              【解决方案13】:

              也许我不理解这个问题,但这听起来直接可行,至少在 bash 中:

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

              这会在括号内运行第一个命令五秒钟,然后将其终止。整个操作同步运行,即当它忙于等待慢速命令时,您将无法使用您的 shell。如果这不是您想要的,应该可以添加另一个 &。

              $! 变量是一个 Bash 内置函数,其中包含最近启动的子 shell 的进程 ID。括号内不要有 & 很重要,这样做会丢失进程 ID。

              【讨论】:

              • 非常聪明!我不知道 bash 可以做到这一点。有人知道 tcsh 的类似技巧吗?
              • 我已经在stackoverflow.com/questions/687948/…询问了您的精彩回答的后续行动@
              • 非常有趣,但是如果进程提前终止并且 PID 被同一用户的另一个不相关的进程重用,则会出现竞争条件。现在我必须阅读系统PAUSE后续的答案,看看他们是否有同样的问题。
              【解决方案14】:

              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 的程序)。对不同解决方案的优缺点有何看法?
              • 嗯,ulimit 不同。我认为它限制了 CPU 时间而不是挂钟时间,这可能很奇怪:如果你将双核机器的两个处理器都挂起来会发生什么?或者如果你睡觉并产生 CPU 时间?乔纳森的程序和我的很相似。他的退出状态是对的,我的不需要编译器。
              【解决方案15】:

              还有ulimit,可以用来限制子进程可用的执行时间。

              ulimit -t 10
              

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

              要实际使用它来限制新进程,而不是当前进程,您可能希望使用包装脚本:

              #! /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。但是有没有办法将其应用于特定的过程?例如,Jonathan 所做的事情是否可以更简单地使用 ulimit 完成?
              • 我已经很久没有使用 ulimit 了。我所做的是有一个运行另一个工具的python脚本: os.system("ulimit -t 600; gtime -f java -Xms1024m -Xmx1024m Main")
              • 谢谢马修!想将其纳入您的答案吗?看起来是 Jonathan 的 C 程序的一个不错的快速而简单的替代方案。甚至可以将其包装在 perl/python/shell 脚本中并具有相同的命令行界面。与 Jonathon 的解决方案相比,它的优点/缺点是什么?
              猜你喜欢
              • 1970-01-01
              • 2019-04-20
              • 2021-11-19
              • 2013-04-12
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多