
flock — блокировки в shell скриптах
Приветствую!
В этой заметке речь пойдет о блокировках файлов в сценариях Bash🔒 с помощью специализированной утилиты — flock
.
Как-то раз у меня была задача написать скрипт с возможностью предотвращения его повторного запуска, если другой инстанс уже запущен. Довольно распространенный кейс. Чаще всего подобное реализуется созданием обычного файла-флага с суффиксом .lock
или типа того. Такой вариант довольно распространен, но далеко не идеален🙂.
Очевидной проблемой может стать, так называемый race condition, по-русски «состояние гонки». Это когда, например, несколько экземпляров скрипта стартуют асинхронно и могут мешать работе друг друга или чего похуже😳.
Для решения данной задачи в скриптах Linux существует стандартная (часто предустановленная) утилита flock
— вот с ней и поработаем🧑💻.
Условно у нас есть script_1.sh
с таким содержимым:
#!/bin/bash
SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"
if [[ -f "$SCRIPT_LOCK" ]]; then
echo "Script 1 is already running. Exiting."
exit 1
else
touch "$SCRIPT_LOCK"
echo "Starting script 1..."
sleep 999
rm -f "$SCRIPT_LOCK"
fi
$BASH_SOURCE[0]
— системная переменная bash, содержащая путь и имя скрипта%.*
— убирает.sh
в имени файла.
На вид все логично и понятно. При запуске:
chmod +x ./script_1.sh
./script_1.sh
Он проверяет наличие lock файла и, если его находит — завершает работу. Если нет — создает его и выполняет дальнейшие действия.
Если во время работы скрипта, в соседнем терминале запустить его повторно, он соответственно поругается, что другой экземпляр уже запущен (lock файл существует) и завершит работу.
При нормальном завершении работы, скрипт удаляет lock файл, чтобы не выпасть в ошибку при следующем запуске.
А если работа скрипта прервется, например, Ctrl+c
и lock файл не будет удален? Такой случай придется обрабатывать отдельно. Об этом мы еще поговорим далее.
Давайте остановим (Ctrl+c
) запущенный скрипт и рассмотрим асинхронный запуск, т.е. создадим условие для race condition:
rm -f ./script_1.lock
./script_1.sh & ./script_1.sh &
Оператор
&
в bash отправляет процесс выполнятся в фоновом режиме.
И увидим, что оба они стартовали успешно?!:
[1] 1696362
[2] 1696363
Starting script 1...
Starting script 1...
Посмотреть процессы, запущенные в текущем сеансе можно с помощью команды jobs:
jobs
Вот она, наша парочка🤨:
[1] - running ./script_1.sh
[2] + running ./script_1.sh
Для решения проблемы можно воспользоваться flock. Создадим 2-й скрипт script_2.sh
такого содержания:
#!/bin/bash
SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"
exec {fd_lock}> "$SCRIPT_LOCK"
if ! flock -n "$fd_lock"; then
echo "Script 2 is already running. Exiting."
exit 1
fi
echo "Staring script 2..."
sleep 999
{fd_lock}>
— синтаксис bash, который создает переменную$fd_lock
и которой присваивает номер ближайшего свободного дескриптора.
В этом варианте мы открываем на запись файл $SCRIPT_LOCK
и присваиваем ему именованный дескриптор — из $fd_lock
(например 10).
После с помощью команды flock
пытаемся заблокировать полученный дескриптор.
Если успешно — продолжаем работу, если нет — завершаем с ошибкой.
При работающем экземпляре скрипта:
chmod +x ./script_2.sh
./script_2.sh
И его повторном запуске наш скрипт также будет ругаться, что присутствует блокировка и завершит работу.
Если прервать работу рабочего инстанса — lock файл не удалится, но это не имеет значения, т.к. блокируется дескриптор. И в случае race condition, как в прошлый раз:
./script_2.sh & ./script_2.sh &
Наш скрипт отработает ожидаемо:
[1] 1734870
[2] 1734871
Staring script 2...
Script 2 is already running. Exiting. 9ms
[2] + 1734871 exit 1 ./script_2.sh
Первый экземпляр запустился, второй завершился ошибкой. Проверяем с помощью jobs
:
jobs
Видим 1 инстанс🥳:
[1] + running ./script_2.sh
Но остается вопрос с физическим lock файлом🤔 Хотелось бы и его удалять корректно… хоть это и не обязательно😉.
Ниже пример кода bash для настройки механизма блокировки с явным закрытием дескриптора для flock
(&-
) и автоматическим удалением .lock
файла, как при корректном завершении скрипта, так и в случае ошибки или прерывания⚡️.
Его можно сохранить, как шаблон и использовать при необходимости. Разумеется все на ваш страх и риск⚠️.
vim script.sh && chmod +x ./script.sh
#!/usr/bin/env bash
SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"
# функция очистки в случае сработки обработчиков
cleanup() {
# отключает все ранее установленные trap-ы
trap - SIGINT SIGTERM ERR EXIT
# закрывает файловый дескриптор блокировки
[[ -n "${fd_lock:-}" ]] && exec {fd_lock}>&-
# удаляет lock-файл, если он принадлежит текущему PID
if [[ -f "$SCRIPT_LOCK" && $(<"$SCRIPT_LOCK") -eq $$ ]]; then
rm -f "$SCRIPT_LOCK"
fi
}
# устанавливает обработчики (trap)
trap cleanup SIGINT SIGTERM ERR EXIT
# открывает файл для записи и сохраняет дескриптор в $fd_lock
exec {fd_lock}>> "${SCRIPT_LOCK}"
# пытается захватить эксклюзивную блокировку (без ожидания)
if ! flock -n "$fd_lock"; then
echo "Script is already running. Exiting."
exit 1
fi
# записывает текущий PID в lock-файл
echo "$$" > "$SCRIPT_LOCK"
echo "Staring script..."
# ваша логика скрипта
sleep 999 # команда для примера
$(<"$SCRIPT_LOCK")
— синтаксис подстановки, позволяющий присвоить переменной содержимое файла;{fd_lock}>>
— оператор дополнения >> используется для предотвращения перезаписи файла;$SCRIPT_LOCK
при повторных запусках.
Если у вас есть более элегантное решение задачи, пожалуйста напишите его в комментариях к посту👇
P.S.
Если честно, то я так до конца и не понял, как именно происходит блокировка через дескрипторы🤯 вроде как в процессе участвует ядро. Ну да ладно.
Другой пример использования flock с «интересными» комментариями можно посмотреть в этой статье Habr’a📝