Команда командной строки для автоматического уничтожения команды через определенное время
Я хотел бы автоматически убить команду через определенное время. Я имею в виду такой интерфейс:
% constrain 300 ./foo args
Который будет запускать "./foo" с "args", но автоматически убивает его, если он все еще работает через 5 минут.
Возможно, было бы полезно обобщить идею для других ограничений, таких как автоматическое уничтожение процесса, если он использует слишком много памяти.
Существуют ли какие-либо инструменты, которые это делают, или кто-нибудь написал такое?
ДОБАВЛЕНО: решение Джонатана - именно то, что я имел в виду, и оно работает как прелесть в Linux, но я не могу заставить его работать на Mac OSX. Я избавился от SIGRTMIN, который позволяет нормально его компилировать, но сигнал просто не отправляется дочернему процессу. Кто-нибудь знает, как заставить это работать на Mac?
[Добавлено: обратите внимание, что от Джонатана доступно обновление, которое работает на Mac и в других местах.]
15 ответов
Я приехал довольно поздно на эту вечеринку, но не вижу своего любимого трюка в ответах.
Под *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 был повторно использован. Вам не нужно, чтобы несколько подпроцессов оболочки работали согласованно, что может быть сделано правильно, но скорее склонно к гонкам.
Недостатки Процесс, ограниченный во времени, не может манипулировать своим будильником (например, alarm(2)
, ualarm(2)
, setitimer(2)
), так как это, вероятно, очистит унаследованную тревогу. Очевидно, что он также не может блокировать или игнорировать SIGALRM, хотя то же самое можно сказать и о SIGINT, SIGTERM и т. Д. Для некоторых других подходов.
Некоторые (очень старые, я думаю) системы реализуют sleep(2)
с точки зрения alarm(2)
и даже сегодня некоторые программисты используют alarm(2)
как грубый механизм внутреннего тайм-аута для операций ввода-вывода и других операций. Однако, по моему опыту, эта техника применима к подавляющему большинству процессов, которые вы хотите ограничить по времени.
GNU Coreutils включает в себя команду timeout, установленную по умолчанию во многих системах.
https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html
Смотреть free -m
на одну минуту, затем убейте его, отправив сигнал TERM:
timeout 1m watch free -m
Может быть, я не понимаю вопроса, но это звучит выполнимо, по крайней мере, в bash:
( /path/to/slow command with options ) & sleep 5 ; kill $!
Это запускает первую команду в круглых скобках в течение пяти секунд, а затем убивает ее. Вся операция выполняется синхронно, то есть вы не сможете использовать свою оболочку, пока она занята, ожидая медленной команды. Если это не то, что вы хотели, должна быть возможность добавить еще один &.
$!
Переменная - это встроенная функция Bash, содержащая идентификатор процесса последней запущенной подоболочки. Важно не иметь & в круглых скобках, так как при этом теряется идентификатор процесса.
Существует также ulimit, который можно использовать для ограничения времени выполнения, доступного подпроцессам.
ulimit -t 10
Ограничивает процесс до 10 секунд процессорного времени.
Чтобы фактически использовать его для ограничения нового процесса, а не текущего процесса, вы можете использовать скрипт-обертку:
#! /usr/bin/env python
import os
os.system("ulimit -t 10; other-command-here")
Командой other может быть любой инструмент. Я запускал версии различных алгоритмов сортировки на Java, Python, C и Scheme и записывал, сколько времени они занимали, в то время как время выполнения ограничивалось 30 секундами. Приложение Какао-Python сгенерировало различные командные строки - включая аргументы - и сопоставило времена в CSV-файл, но на самом деле это просто пух над командой, представленной выше.
У меня есть программа под названием timeout
это делает то, что написано на C, первоначально в 1989 году, но с тех пор периодически обновляется.
Обновление: этот код не может быть скомпилирован в MacOS X, потому что SIGRTMIN не определен, и не имеет тайм-аута при запуске в MacOS X, потому что
signal()
функция там возобновляет wait()
после истечения времени тревоги - что не является обязательным поведением. У меня новая версия timeout.c
которая решает обе эти проблемы (используя sigaction()
вместо signal()
). Как и прежде, свяжитесь со мной для получения 10K gzip-файла 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; /* Dunno what happened! */
else if (WIFEXITED(status))
status = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
status = WTERMSIG(status);
else
status = 2; /* Dunno what happened! */
return(status);
}
Если вам нужен "официальный" код для "stderr.h" и "stderr.c", свяжитесь со мной (см. Мой профиль).
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' - любой командой.
Моя вариация на perl one-liner дает вам статус выхода без использования muck с помощью 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,
В качестве дополнительного преимущества скрипт также завершает работу с тем, что, вероятно, является правильным состоянием выхода.
Команда timeout из Ubuntu/Debian при компиляции из исходного кода для работы на Mac. Дарвин
10.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
Чистый баш:
#!/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 "$@"
Как насчет использования инструмента ожидания?
## 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
}
Попробуйте что-то вроде:
# 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 процессов в секунду. Это можно исправить.
Я использую "timelimit", который является пакетом, доступным в репозитории debian.
Небольшая модификация perl one-liner даст правильный статус выхода.
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
По сути, выход ($? >> 8) переадресует состояние выхода подпроцесса. Я просто выбрал 77 в состоянии выхода для тайм-аута.
Разве нет способа установить определенное время с помощью "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 и присвоить ему переменную. Я не слишком опытен, но предполагаю, что это выполнимо.