С чего начать Первые шаги трек · 15 ступеней
Linux Labs

flock — защита от двойного запуска

7 мин Ubuntu 24.04 · util-linux 2.39

flock держит блокировку файла, пока работает команда: второй экземпляр скрипта ждёт её или сразу выходит.

Шпаргалка

6 команд Версия для печати
  • flock -n /run/lock/clean.lock clean.sh занято — выйти с кодом 1
  • flock -w 30 /run/lock/clean.lock clean.sh ждать до 30 секунд
  • flock /run/lock/clean.lock clean.sh ждать сколько нужно
  • flock -n -E 75 /run/lock/clean.lock cmd занято — код 75
  • flock -n -o /run/lock/app.lock cmd потомки не держат блокировку
  • lslocks -o COMMAND,PID,TYPE,PATH кто держит блокировки

Обзор

Скрипт по расписанию может стартовать, пока прошлый запуск ещё работает: очистка затянулась, кто-то запустил её руками, задача записана и в cron, и в таймере. Два экземпляра удаляют одни и те же файлы и пишут в один журнал вперемешку.

flock открывает файл блокировки, берёт на него блокировку у ядра и только потом запускает команду. Пока команда работает, блокировка занята. Второй flock на тот же файл ждёт или, с ключом -n, сразу выходит с кодом 1. Сообщения он при этом не печатает:

text
student@ubuntu:~$ flock -n /run/lock/job.lock sleep 3 &
student@ubuntu:~$ flock -n /run/lock/job.lock echo second; echo "rc=$?"
rc=1

Получив блокировку, flock возвращает код самой команды.

Как вызван Блокировка занята Код, если не дождался
flock FILE cmd ждёт освобождения —
flock -n FILE cmd сразу выходит 1 или число из -E
flock -w 30 FILE cmd ждёт до 30 секунд 1 или число из -E

Блокировку держит открытый файловый дескриптор. Процесс завершился любым способом, даже от kill -9, — ядро закрыло дескриптор и сняло блокировку. Поэтому после сбоя блокировка не «зависает». Сам файл остаётся на диске, удалять его не нужно.

Блокировка рекомендательная: ядро не запрещает писать в файл и не трогает программы, которые flock не вызывают. Защита работает, когда каждый запуск берёт блокировку на одном и том же файле.

Файл блокировки удобно держать в /run/lock (он же /var/lock): каталог в памяти, писать в него может любой пользователь, после перезагрузки он пуст.

Ключи

10

Основные ключи — в таблице ниже. Полный список — man flock.

Ключ Что делает Пример
-n --nonblock Не ждать; блокировка занята — выйти с кодом 1 без сообщения. flock -n /run/lock/clean.lock clean.sh
-w --timeout SECONDS Ждать блокировку не дольше заданного числа секунд, потом код 1. flock -w 60 /run/lock/backup.lock backup.sh
-E --conflict-exit-code N Код выхода, если блокировку не получили (вместо 1). flock -n -E 75 /run/lock/x.lock cmd
-o --close Закрыть дескриптор перед запуском команды — потомки не держат блокировку. flock -n -o /run/lock/app.lock app-start.sh
-c --command STRING Выполнить строку оболочкой — для нескольких команд под одной блокировкой. flock -n /run/lock/x.lock -c 'step1 && step2'
FD Блокировка на открытый дескриптор скрипта вместо файла и команды. exec 9>/run/lock/x.lock; flock -n 9
-o --output LIST Какие столбцы показать в lslocks; PATH — файл блокировки целиком. lslocks -o COMMAND,PID,TYPE,PATH
-s --shared Общая блокировка — её могут держать несколько читателей сразу. flock -s /run/lock/data.lock cat data.csv
-x --exclusive Исключительная блокировка; так flock работает по умолчанию. flock -x /run/lock/data.lock update.sh
-u --unlock FD Снять блокировку с дескриптора раньше выхода скрипта. flock -u 9

Ничего не найдено. Попробуй запрос короче.

Рецепты

5

Строка crontab без наложений

text
*/5 * * * * flock -n /run/lock/sync.lock /usr/local/bin/sync.sh

Запуск, который пришёлся на работающий прошлый, тихо пропускается.

Блокировка внутри скрипта

bash
#!/bin/bash
exec 9>/run/lock/clean.lock
if ! flock -n 9; then
  echo "cleanup уже запущен" >&2
  exit 1
fi
# дальше — работа; блокировка снимется с выходом скрипта

exec 9>файл открывает дескриптор 9 для всего скрипта, flock -n 9 берёт на него блокировку. Скрипт защищён, как его ни запусти: руками, из cron или таймером. Код при занятой блокировке выбери сам: 1 — попытка видна как сбой в systemctl status и мониторинге, 0 — пропуск считается нормой.

Служба systemd

ini
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/lock/clean.lock /usr/local/bin/clean.sh

Второй экземпляр того же юнита systemd и сам не запустит: пока служба работает, повторный systemctl start присоединяется к текущему запуску. flock нужен, когда тот же скрипт запускают ещё и руками, из cron или другим юнитом. Расписание — в systemd-timers.

Подождать, но не вечно

bash
flock -w 60 /run/lock/backup.lock /usr/local/bin/backup.sh \
  || echo "блокировка занята дольше минуты" >&2

Кто держит блокировку

text
student@ubuntu:~$ lslocks -o COMMAND,PID,TYPE,PATH | grep demo
flock            1031 FLOCK /run/lock/job.lock
student@ubuntu:~$ lsof /run/lock/job.lock
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
flock   1031 student    3rW  REG   0,27        0    6 /run/lock/job.lock
sleep   1033 student    3r   REG   0,27        0    6 /run/lock/job.lock

lsof показывает и процесс-потомок: sleep получил дескриптор от flock и тоже держит блокировку.

Неполадки

4

cannot open lock file: Permission denied

Файл блокировки нельзя открыть. Частый случай: student однажды запустил скрипт руками, файл в /run/lock создан от него, и теперь служба от root получает отказ — в общем каталоге с битом sticky Ubuntu не даёт root открыть чужой файл (fs.protected_regular = 2). flock выходит с кодом 66:

text
student@ubuntu:~$ flock -n /run/lock/p.lock true
student@ubuntu:~$ sudo flock -n /run/lock/p.lock true; echo "rc=$?"
flock: cannot open lock file /run/lock/p.lock: Permission denied
rc=66

Удали файл, пока его никто не держит, и запускай скрипт от одного пользователя. Службе от root можно отдать свой путь: /run/clean.lock.

Второй запуск молча пропал

Так работает -n: занято — код 1 без сообщения. Чтобы пропуск был виден в журнале, допиши || echo "пропуск: прошлый запуск ещё идёт" >&2 или пиши сообщение в скрипте, как в рецепте выше.

failed to execute: No such file or directory

flock не нашёл команду (код 69). В cron и службах PATH короче, чем в терминале, — пиши полный путь: /usr/local/bin/clean.sh.

Блокировка не работает, запуски идут параллельно

Запуски берут блокировку на разных файлах или один из них идёт мимо flock. Сверь путь во всех местах: crontab, ExecStart=, сам скрипт. /var/lock и /run/lock — один каталог.

Опасно

2

Удалить файл блокировки

Пока первый экземпляр держит блокировку, rm убирает файл из каталога. Следующий flock создаёт новый файл и спокойно берёт блокировку на нём — два экземпляра работают одновременно:

text
student@ubuntu:~$ flock -n /run/lock/r.lock sleep 3 &
student@ubuntu:~$ rm /run/lock/r.lock
student@ubuntu:~$ flock -n /run/lock/r.lock echo "second got it"
second got it

Не удаляй файл блокировки — ни руками, ни в конце скрипта.

Фоновый потомок держит блокировку

Дескриптор наследуют все процессы, запущенные командой. Если она оставила процесс в фоне, блокировка живёт, пока жив он, — даже после kill -9 самого flock. Ключ -o закрывает дескриптор перед запуском команды: блокировку держит только flock.

Термины

1
блокировка файла file lock
Отметка ядра на открытом файле: пока процесс её держит, другой процесс такую же не получит. Снимается, когда файл закрыт или процесс завершился. Ещё встречается в: lsof, /proc: процессы и ядро в виде файлов, strace

См. также

  • systemd-timers — запуск скрипта по расписанию.
  • cron — классический планировщик.
  • bash-scripting — скрипт целиком: аргументы, условия, set -e.
  • exit-codes — что значат коды 1, 66 и 69.
  • lsof — какие процессы держат файл.
  • processes — родитель, потомки и наследование дескрипторов.
  • kill — завершить зависший экземпляр.
  • test-conditions — почему [ -f lock ] не защищает от двойного запуска.
Загрузка…