Семафоры и реализация семафора в Bash

Быстрые ссылки
- Что такое семафор?
- Реализовать семафор в Bash: просто или нет?
- Создание семафора в Bash — примеры и шаблоны
- Когда такой подход не работает
- Обзор и рекомендации
Что такое семафор?
Семафор — это примитив синхронизации, который контролирует доступ нескольких потоков или процессов к общему ресурсу, позволяя одновременно использовать ресурс не более N раз (N — целое число). Если N = 1, семафор часто называют бинарным; это выражает идею «только один за раз».
Определение в одну строку: семафор — счётчик, который блокирует или разрешает доступ к ресурсу.
Важно: семафор отличается от mutex тем, что mutex требует, чтобы тот же поток, который взял блокировку, её и освободил; семафор такой гарантии не даёт.
Пример из реальной жизни: мост с подъёмным пролётом и несколькими операторами. Рычаг у каждого оператора — индикатор (переменная) состояния; световой сигнал виден и машинистам, и капитанам судов. Если рычаг поднят — пропускают суда, опущен — машины. Это удобно для визуализации семафора, но в реальном коде требуется атомарность и согласованность.
Реализовать семафор в Bash: просто или нет?
На первый взгляд кажется просто — взять переменную:
BRIDGE=up
if [ "${BRIDGE}" = "down" ]; then
echo "Cars may pass!"
else
echo "Ships may pass!"
fi
BRIDGE=down
if [ "${BRIDGE}" = "down" ]; then
echo "Cars may pass!"
else
echo "Ships may pass!"
fiПроблема: это последовательный код. Пока нет конкуренции, всё работает. Но если два процесса (или фоновые задачи) попытаются менять BRIDGE одновременно, появится условие гонки — race condition. Один процесс может прочитать значение, принять решение, затем второй процесс успеет изменить переменную, и оба выполнят взаимно некорректные операции.
В Bash переменные процесса не разделяются между разными запущенными экземплярами скрипта (каждый процесс имеет собственную память), поэтому для межпроцессного синхронизма нужно использовать механизм, доступный в файловой системе или системные примитивы.
Типичные ошибки и почему простая переменная не работает
- Переменные в отдельных процессах не синхронизируются автоматически.
- Невозможность атомарно проверить и установить значение переменной.
- Выводы (echo) и сообщения не гарантируют консистентности состояния.
- Sleep + polling создаёт большие задержки и не решает атомарности.
Вывод: для надёжного семафора в Bash нужен механизм, гарантирующий атомарную операцию захвата или отказа.
Создание семафора в Bash — подходы
Мы рассмотрим несколько практик:
Простейшая (небезопасная) — общая переменная в рамках одного процесса (бесполезна при множественных процессах).
Atomic directory lock (mkdir) — надёжный, широко применяемый трюк.
flock — работа с файловыми дескрипторами и системной блокировкой.
Счетчик семафора через файл + flock — для семафора с N > 1.
Внешние решения — Redis, sqlite, systemd, IPC primitives — для более надёжных распределённых сценариев.
1) mkdir как атомарный лок
Создание директории является атомарной операцией в POSIX: mkdir возвращает успех только если директория ещё не существовала.
LOCKDIR="/var/lock/my-bridge.lock"
acquire_lock() {
while ! mkdir "$LOCKDIR" 2>/dev/null; do
echo "Lock busy, ожидаю 2 секунды..."
sleep 2
done
echo "Лок взят"
}
release_lock() {
rm -rf "$LOCKDIR"
echo "Лок освобождён"
}
# пример использования
acquire_lock
# критическая секция
echo "Управляю мостом"
sleep 1
release_lockПлюсы: простота и совместимость. Минусы: надо аккуратно удалять директорию при аварии; рекомендуется использовать trap для очистки.
Добавим обработку аварийного завершения:
trap 'rm -rf "$LOCKDIR"; exit' INT TERM EXITНо учтите: если процесс убит сигнaлом SIGKILL, trap не выполнится, и lock останется. В таких случаях можно хранить PID в файле и проверять, жив ли процесс.
2) flock — системный файловый лок
flock использует файловые дескрипторы и даёт нам механизмы блокировок, освобождаемых при завершении процесса.
Простой пример с блокировкой в отдельном блоке:
LOCKFILE="/var/lock/my-bridge.flock"
{
flock -n 9 || { echo "Не удалось получить лок"; exit 1; }
# критическая секция
echo "Выполняю операцию под локом"
sleep 2
} 9>"$LOCKFILE"Или обёртка:
acquire() {
exec 200>"$LOCKFILE"
flock -n 200 || return 1
}
release() {
flock -u 200
exec 200>&-
}
acquire || { echo "Не удалось захватить лок"; exit 1; }
# критическая секция
releaseПлюсы: flock освобождается автоматически, когда процесс завершается или закрывает дескриптор. Минусы: блокировки ограничены файловой системой и не работают между машинами без общей FS.
3) Счётчатый семафор (N > 1) с помощью файла + flock
Идея: держим файл-счётчик, обновляем его под flock атомарно.
COUNT_FILE="/var/lock/bridge-counter"
LOCKFILE="/var/lock/bridge-counter.lock"
MAX=3 # допустимое количество одновременных владельцев
init_counter() {
[ -f "$COUNT_FILE" ] || echo 0 > "$COUNT_FILE"
}
acquire_semaphore() {
exec 201>"$LOCKFILE"
flock 201
count=$(cat "$COUNT_FILE")
if [ "$count" -lt "$MAX" ]; then
count=$((count+1))
echo "$count" > "$COUNT_FILE"
flock -u 201
exec 201>&-
return 0
else
flock -u 201
exec 201>&-
return 1
fi
}
release_semaphore() {
exec 201>"$LOCKFILE"
flock 201
count=$(cat "$COUNT_FILE")
count=$((count-1))
if [ "$count" -lt 0 ]; then count=0; fi
echo "$count" > "$COUNT_FILE"
flock -u 201
exec 201>&-
}
# использование
init_counter
if acquire_semaphore; then
echo "Семафор захвачен"
# критический код
release_semaphore
else
echo "Семафор занят — попробуйте позже"
fiТакой подход компактен, но требует аккуратного управления, особенно при аварийном завершении. Храните PID владельцев в отдельном файле, если нужно автоматически «освобождать» семафор при смерти процесса.
4) FIFO / named pipe — упрощённый консьюмер-производитель
Можно моделировать семафор как очередь токенов в именованном pipe (FIFO). Создаёте FIFO и заранее отправляете N токенов (строк). Каждый процесс читает один токен, используя блокирующее чтение; по завершении возвращает токен назад.
Недостатки: сложнее в управлении, менее распространено в скриптах.
5) Внешние системы (Redis, DB, systemd)
Если требуется распределённый семафор или высокая надёжность, используйте внешние решения:
- Redis с INCR/DECR и Lua-скриптами для атомарных операций.
- SQLite с транзакциями.
- systemd-run / systemd unit locks для сервисов на одной машине.
Плюсы: надёжность, проверенные алгоритмы. Минусы: зависит от внешнего компонента и его доступности.
Анализ исходного примера (мост с операторами)
Исходный псевдо-код (с переменной BRIDGE_SEMAPHORE) не был потокобезопасен. Проблемы:
- Проверка и установка BRIDGE_SEMAPHORE не атомарны.
- Оба процесса могут пройти проверку “если 0” и оба установить 1.
- Sleep + polling означает задержки и возможные ложные срабатывания.
Решение: использовать один из атомарных методов выше. На практике для простых сценариев лучшим выбором будет flock или mkdir.
Полный пример: безопасный lower_bridge с flock и trap
#!/bin/bash
LOCKFILE="/var/lock/bridge.lock"
lower_bridge() {
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "Семофор занят, дождитесь освобождения"
return 1
fi
trap 'echo "Аварийное освобождение лока"; flock -u 9; exec 9>&-; exit 1' INT TERM
echo "Lower bridge command accepted, locking semaphore and lowering the bridge."
# Тут должна быть реализация движения моста
execute_lower_bridge
wait_for_bridge_to_come_down
BRIDGE='down'
echo "Bridge lowered, ensuring at least 5 minutes pass before next allowed bridge movement."
sleep 300
echo "5 Minutes passed, unlocking semaphore (releasing bridge control)"
flock -u 9
exec 9>&-
trap - INT TERM
}Этот код использует системную блокировку и trap для корректной очистки. Однако учтите, что если процесс будет убит SIGKILL, lock всё равно освободится, так как flock привязан к дескриптору процесса и ОС закроет его при завершении процесса.
Когда такой подход не работает (контрпримеры)
- Распределённая система без общей файловой системы: flock и mkdir не синхронизируют процессы на разных машинах.
- Сценарии с сотнями/тысячами операций в секунду — Bash и sleep-петли станут узким местом.
- Жёсткие требования к приоритетам/очередям — простые семафоры не поддерживают приоритеты владельцев.
Альтернатива: использовать Redis, ZooKeeper или базу данных с транзакциями.
Тесты и критерии приёмки (Критерии приёмки)
Критерии приёмки для Bash-семафора:
- Атомарность: невозможно одновременно получить более N разрешений.
- Отказоустойчивость: после аварийного завершения процесс не оставляет живой блокировки.
- Очистка: ресурс освобождается при нормальном завершении.
- Набор тестов: см. раздел «Тест-кейсы».
Тест-кейсы (минимум):
- Запустить 2N процессов, каждый пытается взять семафор N; убедиться, что одновременно активны не больше N.
- Прервать один процесс (SIGKILL) в критической секции и убедиться, что другие процессы позже получат доступ.
- Проверить, что при аварии PID-файлы корректно очищаются или перезаписваются.
- Замерить задержку ожидания при высокой конкуренции.
Чек-лист разработчика и ролей
Роль — Скриптер/Разработчик:
- Выбрать подходящий механизм (mkdir/flock/Redis).
- Реализовать атомарное захватывание и освобождение.
- Добавить trap для очистки.
- Логировать попытки захвата и ошибки.
Роль — Администратор:
- Убедиться, что директория /var/lock доступна.
- Настроить права на файлы блокировок.
- Мониторить «залипшие» lock-файлы или директории.
Роль — QA/Tester:
- Выполнить тест-кейсы конкурентного доступа.
- Проверить поведение при SIGKILL и рестарте системы.
Шаблон SOP для внедрения семафора в продакшен
- Выберите метод: flock для локальной машины, Redis для распределённой среды.
- Напишите обёртку acquire/release с логированием и retry backoff.
- Добавьте unit-тесты и интеграционные сценарии.
- Настройте мониторинг и алерты по «долгим» блокировкам.
- Документируйте эксплуатационные инструкции и rollback для аварийных сценариев.
Модель принятия решений (Mermaid)
flowchart TD
A[Нужен семафор?] -->|Нет| B[Не используем]
A -->|Да| C{Одна машина или несколько?}
C -->|Одна| D[Использовать flock или mkdir]
C -->|Несколько| E{Есть доступ к Redis/DB?}
E -->|Да| F[Использовать Redis/SQL/coordination service]
E -->|Нет| G[Реализовать распределённую логику с heartbeat и PID-файлами]Ментальные модели и эвристики
- Правило простоты: выберите самый простой механизм, который покрывает требования.
- Fail-safe: если возможно утечка лок-файла, реализуйте проверку живости PID-хранителя.
- Цепочка ответственности: в семафоре должен быть понятный владелец и процедура очистки.
Советы по безопасности и надёжности
- Храните lock-файлы в каталоге с ограниченным доступом (например, /var/lock), чтобы предотвратить подделку.
- При использовании PID-файлов проверяйте, принадлежит ли PID ожидаемому пользователю.
- Логи должны содержать временные метки и идентификаторы владельцев блокировки.
Короткий глоссарий (1-строчная)
- Семафор: счётчик, ограничивающий число одновременных владельцев ресурса.
- Mutex: взаимно-исключающая блокировка, которую должен освобождать тот же владелец.
- flock: системная файловая блокировка через файловые дескрипторы.
- mkdir-lock: атомарная блокировка через создание директории.
Локальные альтернативы и подводные камни для Linux
- flock и mkdir работают на локальной POSIX-совместимой FS; NFS и некоторые сетевые файловые системы могут иметь отличия в семантике блокировок.
- На системах с systemd предпочтительнее использовать unit-файлы и systemd-таймеры для планирования операций, вместо сложных скриптов с семафорами.

ALT описания изображений: первое изображение — схема семафора с мостом и операторами; второе изображение — блок-схема функций реализации семафора в Bash (acquire/release/execute).
Когда стоит выбрать другой язык или инструмент
Если у вас:
- требуются миллисекундные задержки и высокая пропускная способность — используйте компилируемые языки (C/C++, Go) с системными IPC;
- распределённая система с многомашинной конкуренцией — используйте Redis, ZooKeeper или консенсусные сервисы;
- нужна транзакционная семантика — используйте СУБД с поддержкой транзакций.
Заключение
Семафор — мощный и простой по концепции инструмент синхронизации. В Bash прямое хранение состояния в переменных не даёт межпроцессной атомарности, поэтому рекомендуются файловые примитивы: mkdir для простых локов или flock для более надёжных системных блокировок. Для семафоров с счётчиком используйте сочетание файла-счётчика и flock. Для распределённых сценариев отдавайте предпочтение внешним сервисам (Redis, БД) или системам оркестрации.
Важно: тестируйте сценарии с аварийным завершением и пиковыми нагрузками.
Краткие рекомендации:
- Для одиночного хоста: flock — лучший выбор.
- Для простых скриптов без обработки SIGKILL: mkdir достаточно.
- Для распределённых или производительных систем: внешний драйвер (Redis/DB).
Если материал был полезен, посмотрите также нашу статью про Assert, Errors и Crashes: What’s the Difference?
Похожие материалы
Служба Print Spooler не запущена — как исправить
Как сделать украшения на 3D‑принтере
ПК не уходит в сон во время стрима
Сброс сети в Windows 10 Anniversary
Raspberry Pi в VirtualBox — быстрая инструкция