flock — блокировки в shell скриптах

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📝

Не забывайте про нашу телегу📱и чат💬
Всех благ✌️

That should be it. If not, check the logs 🙂

Подписаться
Уведомить о
0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии