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

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

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

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

8 ответов


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

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

timeout 10 ping www.goooooogle.com

если вы не хотите что-то загружать, сделайте то, что timeout делает внутренне:

( 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 )

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

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

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

  1. переменная секунд в 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

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

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


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

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


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

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

run_with_timeout ()
{
  t=
  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.


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

run_with_timeout ()
{
  t= ; 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

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