У меня есть сценарий bash, запускающий дочерний процесс, который время от времени терпит крах (фактически зависает) без видимых причин (с закрытым исходным кодом, поэтому я мало что могу с этим поделать). В результате я хотел бы иметь возможность запускать этот процесс на заданное время и завершать его, если он не вернул успешный результат через заданное время. Есть ли простой и надежный способ добиться этого с помощью bash?
Ответ 1
Если вы не возражаете против загрузки чего-либо, используйте 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 )
Ответ 2
# Породить дочерний процесс:
(dosmth) & pid=$!
# в фоновом режиме, спать в течение 10 секунд, затем завершить этот процесс
(sleep 10 && kill -9 $pid) &
И получить коды выхода:
# Породить дочерний процесс:
(dosmth) & pid=$!
# в фоновом режиме, спать в течение 10 секунд, затем завершить этот процесс
(sleep 10 && kill -9 $pid) & waiter=$!
# подождать наш рабочий процесс и вернуть код выхода
exitcode=$(wait $pid && echo $?)
# завершить подпрограмму подпроцесса, если она все еще запущена
kill -9 $waiter 2>/dev/null
# 0, если мы завершили подпроцесс, что означает, что процесс завершился раньше, чем подпроцесс
finished_gracefully=$?
Ответ 3
У меня тоже был такой вопрос, и я нашел две очень полезные вещи:
Переменная 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» (тайм-аут)).
Ответ 4
Один из способов — запустить программу во вложенной оболочке и общаться с ней через именованный канал с помощью команды read. Таким образом можно проверить состояние выхода запущенного процесса и передать его обратно по пайпу.
Вот пример тайминга команды «yes» через 3 секунды: она получает PID процесса с помощью pgrep (возможно, работает только в Linux). Существует также некоторая проблема с использованием пайпа в том, что процесс, открывающий пайп для чтения, будет висеть до тех пор, пока он не будет открыт для записи, и наоборот. Поэтому, чтобы предотвратить зависание команды read, я «заклинил» открытие пайпа для read с помощью фоновой подпрограммы (другой способ предотвратить зависание — открыть пайп для чтения-записи, т. е. read -t 5 <>finished.pipe — однако это также может не работать в некоторых системах, за исключением Linux).
rm -f finished.pipe
mkfifo finished.pipe
{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!
# получение PID
while : ; do
PID=$( pgrep -P $SUBSHELL yes )
test "$PID" = "" || break
sleep 1
done
# открываем пайп на запись
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &
read -t 3 FINISHED <finished.pipe
if [ "$FINISHED" = finished ] ; then
echo 'подпроцесс завершен'
else
echo 'тайм-аут подпроцесса'
kill $PID
fi
rm finished.pipe
Ответ 5
Вот попытка избежать завершения процесса, что уменьшает вероятность завершения другого процесса с тем же идентификатором (хотя полностью избежать такой ситуации, вероятно, невозможно):
run_with_timeout () {
t=$1
shift
echo "running \"$*\" with timeout $t"
(
# сначала запустите процесс в фоновом режиме
(exec sh -c "$*") &
pid=$!
echo $pid
# тайм-аут оболочки
(sleep $t ; echo timeout) &
waiter=$!
echo $waiter
# наконец, позвольте процессу завершиться естественным образом
wait $pid
echo $?
) \
| (read pid
read waiter
if test $waiter != timeout ; then
read status
else
status=timeout
fi
# если мы вышли по таймеру, завершите процесс
if test $status = timeout ; then
kill $pid
exit 99
else
# если программа завершилась нормально, завершите ожидающую оболочку
kill $waiter
exit $status
fi
)
}
Используйте, например, run_with_timeout 3 sleep 10000, который запускает sleep 10000, но завершает его через 3 секунды. Это похоже на другие ответы, которые используют фоновый процесс тайм-аута, чтобы завершить дочерний процесс после задержки.
После завершения этой программы все еще будет запущено несколько «спящих» процессов, но они должны быть безвредны. Это решение может быть лучше, поскольку в нем не используется ни портовая функция оболочки read -t, ни pgrep.
Ответ 6
Этот ответ обрабатывает сигнальные прерывания и очищает фоновые процессы при получении SIGINT. Он использует трюк $BASHPID и exec, использованный в предыдущем ответе, чтобы получить PID процесса (в данном случае $$ в вызове sh). Он использует FIFO для связи с подпрограммой, которая отвечает за уничтожение и очистку (это похоже на пайп в других ответах, но наличие именованного пайпа означает, что обработчик сигналов тоже может писать в него).
run_with_timeout () {
t=$1 ; shift
trap cleanup 2
F=$$.fifo ; rm -f $F ; mkfifo $F
# сначала запустите основной процесс в фоновом режиме
"$@" & pid=$!
# спящий процесс завершает работу
( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
read sleeper <$F
# управление оболочкой - чтение из fifo.
# окончательный ввод - "готово"
# после этого мы можем получить тайм-аут или сигнал.
( 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 $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
поскольку она пытается завершить процесс, который уже завершился сам по себе.
Linux