Запуск программы по расписанию в ОС семейста Unix делается cron
'ом. Достаточно с помощью утилиты crontab
добавить в расписание строку, чтобы указанная команда с указанной периодичностью запускалась демоном cron
.
Вот пример файла расписания с комментариями к полям и единственной командой /home/ay/todo.sh
, запускаемой каждые 10 минут:
# +----------------------------- minute (0 - 59)
# | +------------- hour (0 - 23)
# | | +---------- day of month (1 - 31)
# | | | +------- month (1 - 12)
# | | | | +---- day of week (0 - 6) (Sunday=0 or 7)
# | | | | |
# v v v v v command to be executed
0,10,20,30,40,50 * * * * /home/ay/todo.sh
Но что, если периодически должна запускаться программа, требующая исключительного (единоличного) доступа к некоторому ресурсу?
Если /home/ay/todo.sh
именно такая программа и если может случиться так, что ее выполнение займет более 10 минут, то одновременно окажутся запущенными два процесса, конкурирующие за один ресурс. Чего нельзя допустить!
Программа будет запускаться с помощью shell-скрипта, который и обеспечит выполнение не более одного ее экземпляра одновременно:
ay@tsuki:~$ cat runner0.sh
#!/bin/sh
MY_FLAG=/tmp/$1.flag
MY_COMMAND=$2
[ -f $MY_FLAG ] && exit 1
touch $MY_FLAG
$MY_COMMAND
rm $MY_FLAG
Перед запуском команды, переданной скрипту через второй параметр, скрипт проверяет наличие флага - пустого файла в каталоге /tmp
с именем, определяемым первым параметром. Если файл присутствует, то скрипт завершает работу без запуска программы. Если файла нет, то скрипт создает его, запускает программу на выполнение, а после ее завершения удаляет файл. Таким образом, секция кода от создания флага до его удаления может выполняться не более, чем одним процессом одновременно.
Протестирую работу runner0.sh
, запустив с его помощью на выполнение команду sleep 10
(команда sleep
без побочных эффектов, и, что немаловажно, позволяет задать время ее выполнения):
ay@tsuki:~$ ./runner0.sh runner0 "sleep 10" || echo "failed to run"
ay@tsuki:~$
Успешно.
Теперь протестирую одновременный запуск более одного процесса:
ay@tsuki:~$ ./runner0.sh runner0 "sleep 10" || echo "failed to run" &
[1] 2336
ay@tsuki:~$ ./runner0.sh runner0 "sleep 10" || echo "failed to run"
failed to run
ay@tsuki:~$ ./runner0.sh runner0 "sleep 10" || echo "failed to run"
failed to run
ay@tsuki:~$
[1] + Done ./runner0.sh runner0 "sleep 10" || echo "failed to run" &
ay@tsuki:~$
Пока процесс 2336
, запущенный в фоновом режиме, выполнялся, запустить еще один процесс с флагом runner0
, не удалось. Тест прошел успешно.
Скрипт, подобный runner0.sh
, отработал несколько недель, запускамый cron
'ом по расписанию, когда одним прекрасным утром выяснилось, что флаг в каталоге /tmp
, созданный пару дней назад, почему-то не был удален! Очевидно, произошел сбой при работе скрипта и он не отработал до конца и не удалил флаг. В результате, флаг не позволяет запускать программу, хотя программа в данный момент не выполняется.
Эту ситуацию легко воcпроизвести:
ay@tsuki:~$ touch /tmp/runner0.flag
ay@tsuki:~$ ./runner0.sh runner0 "sleep 10" || echo "failed to run"
failed to run
ay@tsuki:~$ ./runner0.sh runner0 "sleep 10" || echo "failed to run"
failed to run
ay@tsuki:~$
Прекратить безобразие просто (но для этого нужно его заметить):
ay@tsuki:~$ rm /tmp/runner0.flag
ay@tsuki:~$ ./runner0.sh runner0 "sleep 10" || echo "failed to run"
ay@tsuki:~$
Как можно исключить подобную проблему в скрипте, запускаемом cron
'ом? Кроме проверки наличия флага, запускающий скрипт должен убедиться, что охраняемый флагом процесс все еще выполняется. А если не выполняется, то запускать программу на выполнение.
Новая версия скритпа:
ay@tsuki:~$ cat runner1.sh
#!/bin/sh
MY_FLAG=/tmp/$1.flag
MY_COMMAND=$2
check_flag()
{
# check that only one process is running
if [ -s ${MY_FLAG} ]
then
MY_PID=`cat ${MY_FLAG}`
if ( ps -e -o pid | grep -q $MY_PID )
then
# process PID is still running
exit 1
else
# re-create PID file
echo $$ > ${MY_FLAG}
fi
else
# create PID file
echo $$ > ${MY_FLAG}
fi
}
check_flag
$MY_COMMAND
rm $MY_FLAG
Теперь в файл-флаг помещается идентификатор текущего процесса, а проверка состоит в выяснении, есть ли среди текущих выполняющихся процессов процесс с сохраненным идентификатором.
Протестирую общую работоспособность runner1.sh
:
ay@tsuki:~$ ./runner1.sh runner1 "sleep 10" || echo "failed to run"
ay@tsuki:~$ ./runner1.sh runner1 "sleep 10" || echo "failed to run" &
[1] 2354
ay@tsuki:~$ ./runner1.sh runner1 "sleep 10" || echo "failed to run"
failed to run
ay@tsuki:~$ ./runner1.sh runner1 "sleep 10" || echo "failed to run"
failed to run
ay@tsuki:~$
[1] + Done ./runner1.sh runner1 "sleep 10" || echo "failed to run" &
ay@tsuki:~$
А теперь протестирую работу скрипта в ситуации, с которой не справляется runner0.sh
. В файл /tmp/runner1.flag
помещу идентификатор процесса, который отработал и завершился (это процесс интерпретатора bash
, запущенного из командной строки):
ay@tsuki:~$ bash -c 'echo $$ > /tmp/runner1.flag'
ay@tsuki:~$ cat /tmp/runner1.flag
2208
ay@tsuki:~$ ./runner1.sh runner1 "sleep 10" || echo "failed to run"
ay@tsuki:~$
Скрипт runner1.sh
запустил на выполнение команду, несмотря на наличие флага в каталоге /tmp
. И после выполнения скрипта флага больше нет:
ay@tsuki:~$ ls -l /tmp/runner1.flag
/tmp/runner1.flag not found
Скрипт, подобный runner1.sh
, отработал несколько недель, запускамый cron
'ом по расписанию, когда однажды вечером выяснилось, что запущенный скриптом процесс фатально завис! Процесс, идентификатор которого хранит флаг, был запущен больше суток назад, и до сих пор не завершился.
Это аномальная ситуация, требующая завершить процесс принудительно (после чего runner1.sh
снова запустит программу при следующеми вызове cron
'ом):
ay@tsuki:~$ cat /tmp/runner1.flag
2556
ay@tsuki:~$ kill -9 2556
ay@tsuki:~$
И вновь безобразие устраняется просто, после того, как замечено. Можно ли усовершенствовать запускающий скрипт, чтобы он учитывал возможность зависания запущенной программы?
Отведем на выполнение программы некоторое конечное время, по истечении которого будем принудительно завершать процесс. Для этого добавим в скрипт runner1.sh
функции timeout_handler
и run_with_timeout
и сохраним скрипт под именем runner2.sh
:
ay@tsuki:~$ cat runner2.sh
#!/bin/sh
MY_FLAG=/tmp/$1.flag
MY_COMMAND=$2
MY_TIMEOUT=$3
# USR1 signal handler
timeout_handler()
{
if [ "$MY_COMMAND_PID" != "" ]
then
kill -9 $MY_COMMAND_PID 2> /dev/null
fi
}
run_with_timeout()
{
if [ "$MY_TIMEOUT" != "0" ]
then
trap timeout_handler USR1
# send USR1 to this process after sleeping in background for MY_TIMEOUT seconds
sleep $MY_TIMEOUT && kill -s USR1 $$ &
MY_KILLER_PID=$!
fi
# run the process in background
if [ "$MY_COMMAND" != "" ]
then
$MY_COMMAND &
# and remember PID of the last background process
MY_COMMAND_PID=$!
fi
# wait for the background process
if [ "$MY_COMMAND_PID" != "" ]
then
wait $MY_COMMAND_PID
fi
# reset signal handler and kill the sleeping killer
if [ "$MY_TIMEOUT" != "0" ]
then
unset MY_COMMAND_PID
trap "" USR1
kill $MY_KILLER_PID 2> /dev/null
fi
}
check_flag()
{
# check that only one process is running
if [ -s ${MY_FLAG} ]
then
# check if PID is running
MY_PID=`cat ${MY_FLAG}`
if ( ps -e -o pid | grep -q $MY_PID )
then
exit 1
else
# re-create PID file
echo $$ > ${MY_FLAG}
fi
else
# create PID file
echo $$ > ${MY_FLAG}
fi
}
check_flag
run_with_timeout
rm $MY_FLAG
Функция run_with_timeout
, в отличие от прежних версий скрипта, запускает программу в фоновом режиме и далее ожидает ее завершения с помощью команды wait
. Такой подход потенциально позволяет запустить на выполнение более одной команды параллельно, добавляя, через пробел, идентификаторы запущенных процессов в переменную MY_COMMAND_PID
, и далее ожидать их завершения. Но пока мне достаточно запуска одной команды.
Протестирую общую работоспособность скрипта runner2.sh
, передавая для третьего параметра значение 10 (секунд), превышающее время выполнения команды sleep 5
:
ay@tsuki:~$ ./runner2.sh runner2 "sleep 5" 10 || echo "failed to run"
ay@tsuki:~$ ./runner2.sh runner2 "sleep 5" 10 || echo "failed to run" &
[1] 2932
ay@tsuki:~$ ./runner2.sh runner2 "sleep 5" 10 || echo "failed to run"
failed to run
ay@tsuki:~$ ./runner2.sh runner2 "sleep 5" 10 || echo "failed to run"
[1] + Done ./runner2.sh runner2 "sleep 5" 10 || echo "failed to run" &
ay@tsuki:~$
ay@tsuki:~$ bash -c 'echo $$ > /tmp/runner2.flag'
ay@tsuki:~$ cat /tmp/runner2.flag
2712
ay@tsuki:~$ ./runner2.sh runner2 "sleep 5" 10 || echo "failed to run"
ay@tsuki:~$ ls /tmp/runner2.flag
/tmp/runner2.flag not found
А теперь протестирую принудительное завершение "зависшего" процесса по истечении времени, отпущенного на его выполнение:
ay@tsuki:~$ date; (./runner2.sh runner1 "sleep 5" 3 || echo "failed to run"); date
Fri Jan 17 14:14:41 VLAT 2015
Fri Jan 17 14:14:44 VLAT 2015
Успешно! Запущенный процесс был завершен через 3 секунды после начала выполнения, что видно из вывода команд date
, обрамляющих вызов runner2.sh
.
Уже в течение трех недель скрипт, подобный runner2.sh
, в расписании cron
'а запускает программы. Пока полет нормальный. Интересно, будут ли еще сюрпризы?
Комментариев нет:
Отправить комментарий