Как убить дочерний процесс после заданного таймаута в Bash?

178

У меня есть сценарий bash, который запускает дочерний процесс, который время от времени завершается сбоем (фактически зависает) без видимой причины (с закрытым исходным кодом, поэтому я ничего не могу с этим поделать). В результате я хотел бы иметь возможность запускать этот процесс в течение определенного периода времени и завершать его, если он не завершился успешно через определенное время.

Есть ли простой и надежный способ добиться этого с помощью bash?

PS: скажите, подходит ли этот вопрос лучше для serverfault или superuser.

Greg
источник
Связанный: stackoverflow.com/q/601543/132382
Пилкроу
Очень полный ответ здесь: stackoverflow.com/a/58873049/2635443
Орсирис де Йонг

Ответы:

260

(Как видно из записи BASH FAQ # 68: «Как запустить команду и отменить ее (время ожидания) через N секунд?» )

Если вы не возражаете против загрузки чего-либо, используйте timeout( sudo apt-get install timeout) и используйте это так: (в большинстве систем это уже установлено, в противном случае используйте sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Если вы не хотите что-то скачивать, делайте то, что делает внутреннее время:

( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )

В случае, если вы хотите сделать тайм-аут для более длинного кода Bash, используйте второй параметр как таковой:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )
Игнасио Васкес-Абрамс
источник
8
Ответ Ре Игнасио на тот случай, если кому-то еще будет интересно, что я сделал: cmdpid=$BASHPIDне возьмет pid вызывающей оболочки, а (первую) подоболочку, которая запущена (). Эта (sleepштука вызывает второй подоболочек в первом подоболочке, чтобы подождать 10 секунд в фоновом режиме и убить первый подоболочек, который после запуска процесса подоболочки-убийцы продолжает выполнять свою рабочую нагрузку ...
jamadagni
17
timeoutявляется частью GNU coreutils, поэтому должен быть уже установлен во всех системах GNU.
Самер
1
@Sameer: ​​Только начиная с версии 8.
Игнасио Васкес-Абрамс
3
Я не уверен на 100% в этом, но, насколько я знаю (и я знаю, что говорила моя страница timeoutруководства ) , теперь является частью coreutils.
Бенариорг
5
Эта команда не «заканчивает рано». Он всегда будет убивать процесс во время ожидания - но не справится с ситуацией, когда он не остановился.
Соколиный глаз
28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

или также получить коды выхода:

# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?
Дэн
источник
8
Вы не должны использовать, kill -9прежде чем попробовать сигналы, которые процесс может обработать в первую очередь.
Приостановлено до дальнейшего уведомления.
Правда, я собирался быстро исправить ситуацию и просто предположил, что он хочет немедленного прекращения процесса, потому что он сказал, что он выходит из строя
Дан
8
Это на самом деле очень плохое решение. Что если dosmthв течение 2 секунд завершится, другой процесс получит старый pid, а вы убьете новый?
Телепортирующаяся коза
ПИД-утилизация работает, достигнув предела и обернувшись вокруг. Весьма маловероятно, что другой процесс повторно использует PID в течение оставшихся 8 секунд, если только система полностью не работает.
Киттидор
13
sleep 999&
t=$!
sleep 10
kill $t
DigitalRoss
источник
Это влечет за собой чрезмерное ожидание. Что если настоящая команда ( sleep 999здесь) часто завершается быстрее, чем навязанный сон ( sleep 10)? Что если я хочу дать ему шанс до 1 минуты 5 минут? Что, если у меня в сценарии будет куча таких случаев :)
it3xl
3

У меня также был этот вопрос и я нашел еще две очень полезные вещи:

  1. Переменная SECONDS в bash.
  2. Команда "pgrep".

Поэтому я использую что-то вроде этого в командной строке (OSX 10.9):

ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done

Поскольку это цикл, я включил «0.2 сна», чтобы процессор остыл. ;-)

(Кстати: ping - плохой пример в любом случае, вы просто использовали бы встроенную опцию -t (timeout).)

Ulrich
источник
1

Предполагая, что у вас есть (или вы можете легко создать) файл pid для отслеживания pid ребенка, вы можете затем создать сценарий, который проверяет время изменения файла pid и при необходимости убивает / запускает процесс. Затем просто поместите скрипт в crontab, чтобы он выполнялся примерно в нужный период.

Дайте мне знать, если вам нужно больше деталей. Если это не похоже на то, что вам нужно, как насчет выскочки?

Кодзиро
источник
1

Одним из способов является запуск программы в подоболочке и связь с подоболочкой через именованный канал с помощью readкоманды. Таким образом, вы можете проверить состояние выхода запускаемого процесса и сообщить об этом через канал.

Вот пример тайм-аута yesкоманды через 3 секунды. Он получает PID процесса, который используется pgrep(возможно, работает только в Linux). Существует также некоторая проблема с использованием канала в том, что процесс, открывающий канал для чтения, будет зависать до тех пор, пока он не будет открыт для записи, и наоборот. Поэтому, чтобы предотвратить readзависание команды, я «заклинил» открытие канала для чтения с помощью фоновой подоболочки. (Еще один способ предотвратить остановку, чтобы открыть канал чтения-записи, т. read -t 5 <>finished.pipeЕ., Однако, это также может не работать, кроме как с Linux.)

rm -f finished.pipe
mkfifo finished.pipe

{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!

# Get command PID
while : ; do
    PID=$( pgrep -P $SUBSHELL yes )
    test "$PID" = "" || break
    sleep 1
done

# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &  

read -t 3 FINISHED <finished.pipe

if [ "$FINISHED" = finished ] ; then
  echo 'Subprocess finished'
else
  echo 'Subprocess timed out'
  kill $PID
fi

rm finished.pipe
Гэвин Смит
источник
0

Вот попытка избежать попытки завершить процесс после того, как он уже завершился, что уменьшает вероятность уничтожения другого процесса с тем же идентификатором процесса (хотя, вероятно, невозможно полностью избежать такого рода ошибок).

run_with_timeout ()
{
  t=$1
  shift

  echo "running \"$*\" with timeout $t"

  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid

  # the timeout shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter

  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter

     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi

     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting shell
       kill $waiter
       exit $status
     fi
  )
}

Используйте лайк run_with_timeout 3 sleep 10000, который запускается, sleep 10000но заканчивается через 3 секунды.

Это похоже на другие ответы, которые используют фоновый процесс тайм-аута, чтобы убить дочерний процесс после задержки. Я думаю, что это почти то же самое, что расширенный ответ Дэна ( https://stackoverflow.com/a/5161274/1351983 ), за исключением того, что оболочка тайм-аута не будет уничтожена, если она уже закончилась.

После того, как эта программа закончилась, все еще будет несколько запущенных «спящих» процессов, но они должны быть безвредными.

Это может быть лучшим решением, чем мой другой ответ, потому что он не использует функцию непереносимой оболочки read -tи не использует pgrep.

Гэвин Смит
источник
Какая разница между (exec sh -c "$*") &а sh -c "$*" &? В частности, зачем использовать первое вместо второго?
Джастин С
0

Вот третий ответ, который я представил здесь. Этот обрабатывает прерывания сигнала и очищает фоновые процессы при SIGINTполучении. Он использует $BASHPIDи execтрюк , используемый в верхнем ответе , чтобы получить PID процесса (в данном случае $$в shвызове). Он использует FIFO для связи с подоболочкой, которая отвечает за убийство и очистку. (Это похоже на канал в моем втором ответе , но наличие именованного канала означает, что обработчик сигнала также может писать в него.)

run_with_timeout ()
{
  t=$1 ; shift

  trap cleanup 2

  F=$$.fifo ; rm -f $F ; mkfifo $F

  # first, run main process in background
  "$@" & pid=$!

  # sleeper process to time out
  ( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
  read sleeper <$F

  # control shell. read from fifo.
  # final input is "finished".  after that
  # we clean up.  we can get a timeout or a
  # signal first.
  ( exec 0<$F
    while : ; do
      read input
      case $input in
        finished)
          test $sleeper != 0 && kill $sleeper
          rm -f $F
          exit 0
          ;;
        timeout)
          test $pid != 0 && kill $pid
          sleeper=0
          ;;
        signal)
          test $pid != 0 && kill $pid
          ;;
      esac
    done
  ) &

  # wait for process to end
  wait $pid
  status=$?
  echo finished >$F
  return $status
}

cleanup ()
{
  echo signal >$$.fifo
}

Я старался избегать условий гонки, насколько смог. Тем не менее, один из источников ошибок, которые я не смог удалить, - это когда процесс заканчивается примерно в то же время, что и время ожидания. Например, run_with_timeout 2 sleep 2или run_with_timeout 0 sleep 0. Для меня последнее дает ошибку:

timeout.sh: line 250: kill: (23248) - No such process

как он пытается убить процесс, который уже вышел сам по себе.

Гэвин Смит
источник