Иногда бывает нужно сделать так, чтобы в каждый момент времени работало не больше одного экземпляра вашего bash скрипта. Если на вашей платформе есть команда flock, то это сделать достаточно просто:
#!/bin/bash LOCK_FILE=/tmp/my-script.lock LOCK_FD=9 get_lock() { # need to use eval here for proper expansion eval "exec $LOCK_FD>$LOCK_FILE" flock -n $LOCK_FD } get_lock || exit # ...
Используя этот подход необходимо помнить, что все дочерние процессы наследуют дескрипторы файлов, открытых родительским процессом. У меня был скрипт, который запускался из крона. Этот скрипт стартовал ssh-agent, если он еще не был запущен, и выполнял через ssh команды на нескольких серверах. ssh-agent наследовал дескриптор лок файла и как следствие скрипт выполнялся только один раз при запуске ssh-agent. Для избежания подобной ситуации необходимо явно закрыть лок файл при вызове команды, которая порождает дочерний процесс. В моем случае пришлось сделать так:
#!/bin/bash LOCK_FILE=/tmp/my-script.lock LOCK_FD=9 SSH_KEY=/root/.ssh/id_rsa.for.ssh-agent get_lock() { # need to use eval here for proper expansion eval "exec $LOCK_FD>$LOCK_FILE" flock -n $LOCK_FD } get_lock || exit socket=$(find /tmp/ssh-*/agent.* -user root 2>/dev/null || true) if [ -z "$socket" ]; then # need to use eval here for proper expansion # we need to close explicitly fd of the lock file # otherwise open fd is kept by ssh-agent and lock can't be aquired until ssh-agent exits eval ". <(ssh-agent $LOCK_FD>&-)" ssh-add $SSH_KEY return else # ... fi #...
Если по какой-то причине вы не можете использовать flock необходимую функциональность можно реализовать используя исключительно bash:
#!/bin/bash set -u PID_LIST=/tmp/test-get-lock.pid get_lock() { local pid while true; do while read pid; do kill -0 $pid || continue [ "$pid" != "$BASHPID" ] && return 1 echo $BASHPID >$PID_LIST.new && mv $PID_LIST.new $PID_LIST && return 0 done < $PID_LIST echo $BASHPID >>$PID_LIST done } if get_lock 2>/dev/null; then sleep 1 pids="$(cat $PID_LIST)" pid=$(echo "$pids"|head -n1) [ "$BASHPID" != "$pid" ] && echo "pid: $BASHPID unexpected pid: $pid $pids" echo "pid: $BASHPID get_lock success" else echo "pid: $BASHPID get_lock failed" fi
Вот как это работает:
- Идентификаторы процессов (pid-ы) находятся в файле. Мы читаем pid-ы из файла и проверяем соответствуют ли они выполняющимся процессам..
- pid-ы завершенных процессов игнорируются
- Обнаружение pid-а выполняющегося процесса, который не соответствует текущему процессу, означает, что уже выполняется другой экземпляр скрипта и мы сообщаем о невозможности выполнения.
- Если мы встретили pid соответствующий текущему процессу мы удаляем из файла, хранящего pid-ы, все кроме текущего pid-а (mv это атомарная операция) и продолжаем выполнение скрипта.
- Если мы вышли из цикла проверки pid-ов мы дописываем текущий pid в конец файла и повторяем проверку. Дописывание в конец файла это атомарная операция.
Насколько это решение надежно? В процессе отладки я использовал следующую команду для тестирования:
rm -f /tmp/*.log for x in {0000..9999}; do ./lock-test.sh >/tmp/$x.log 2>&1 & done wait echo "success: $(grep success /tmp/*.log|wc -l), failure: $(grep failed /tmp/*.log|wc -l), unexpected pid: $(grep unexpected /tmp/*.log|wc -l)"
Отсутствие неожиданных pid-ов означало, что код работал правильно. Для финального тестирования я использовал вот такую команду:
for y in {000..999}; do echo -n " $y" bash -c 'rm -f /tmp/*.log for x in {0000..9999}; do ./lock-test.sh >/tmp/$x.log 2>&1 & done wait' 2>/dev/null; grep unexpected /tmp/*.log && break done
Я прогнал этот тест на своем ноутбуке с 4-х ядерным i7, на виртуальной машине с 2-мя ядрами и на сервере с 24-мя ядрами. Ни в одном из случаев проблем обнаружен не было. Тем не менее я допускаю, что мое тестирование было не исчерпывающим и предлагаемый код может сработать неправильно при каком-то стечении обстоятельств. Впрочем, если вы будете использовать данный код, для того, чтобы скрипт, запускаемый из крона, работал в единственном экземпляре, с большой вероятностью проблем не будет.
