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

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

Существует ли простой и надежный способ использования bash?

P.S.: Скажите, если этот вопрос лучше подходит для serverfault или суперпользователя.

Ответ 1

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

Если вы не хотите что-то скачивать, используйте timeout (sudo apt-get install timeout) и используйте его как:

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 )

Ответ 2

# 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=$?

Ответ 3

sleep 999&
t=$!
sleep 10
kill $t

Ответ 4

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

  • Переменная SECONDS в bash.
  • Команда "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

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

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

Ответ 5

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

Сообщите мне, если вам нужна дополнительная информация. Если это не похоже на то, что вам подходит, а что насчет upstart?

Ответ 6

Один из способов - запустить программу в подоболочке и связаться с подоболочкой через именованный канал с помощью команды 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

Ответ 7

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

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 секунды.

Это похоже на другие ответы, которые используют процесс тайм-аута в фоновом режиме, чтобы убить дочерний процесс после задержки. Я думаю, что это почти то же самое, что и ответ Дэн (fooobar.com/questions/52302/...), за исключением того, что оболочка тайм-аута не будет убита, если она уже закончилась.

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

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

Ответ 8

Вот третий ответ, который я представил здесь. Он обрабатывает прерывания сигнала и очищает фоновые процессы при приеме 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
  "[email protected]" & 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

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