Гид по технологиям

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

8 min read Bash Обновлено 01 Dec 2025
Семафоры в Bash: что это и как реализовать
Семафоры в 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 — подходы

Мы рассмотрим несколько практик:

  1. Простейшая (небезопасная) — общая переменная в рамках одного процесса (бесполезна при множественных процессах).

  2. Atomic directory lock (mkdir) — надёжный, широко применяемый трюк.

  3. flock — работа с файловыми дескрипторами и системной блокировкой.

  4. Счетчик семафора через файл + flock — для семафора с N > 1.

  5. Внешние решения — 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 разрешений.
  • Отказоустойчивость: после аварийного завершения процесс не оставляет живой блокировки.
  • Очистка: ресурс освобождается при нормальном завершении.
  • Набор тестов: см. раздел «Тест-кейсы».

Тест-кейсы (минимум):

  1. Запустить 2N процессов, каждый пытается взять семафор N; убедиться, что одновременно активны не больше N.
  2. Прервать один процесс (SIGKILL) в критической секции и убедиться, что другие процессы позже получат доступ.
  3. Проверить, что при аварии PID-файлы корректно очищаются или перезаписваются.
  4. Замерить задержку ожидания при высокой конкуренции.

Чек-лист разработчика и ролей

Роль — Скриптер/Разработчик:

  • Выбрать подходящий механизм (mkdir/flock/Redis).
  • Реализовать атомарное захватывание и освобождение.
  • Добавить trap для очистки.
  • Логировать попытки захвата и ошибки.

Роль — Администратор:

  • Убедиться, что директория /var/lock доступна.
  • Настроить права на файлы блокировок.
  • Мониторить «залипшие» lock-файлы или директории.

Роль — QA/Tester:

  • Выполнить тест-кейсы конкурентного доступа.
  • Проверить поведение при SIGKILL и рестарте системы.

Шаблон SOP для внедрения семафора в продакшен

  1. Выберите метод: flock для локальной машины, Redis для распределённой среды.
  2. Напишите обёртку acquire/release с логированием и retry backoff.
  3. Добавьте unit-тесты и интеграционные сценарии.
  4. Настройте мониторинг и алерты по «долгим» блокировкам.
  5. Документируйте эксплуатационные инструкции и 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-таймеры для планирования операций, вместо сложных скриптов с семафорами.

Пример реализации семафора в Bash: диаграмма функций и вызовов

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?

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Служба Print Spooler не запущена — как исправить
Руководство

Служба Print Spooler не запущена — как исправить

Как сделать украшения на 3D‑принтере
3D-печать

Как сделать украшения на 3D‑принтере

ПК не уходит в сон во время стрима
Руководство

ПК не уходит в сон во время стрима

Сброс сети в Windows 10 Anniversary
Windows

Сброс сети в Windows 10 Anniversary

Raspberry Pi в VirtualBox — быстрая инструкция
Инструкции

Raspberry Pi в VirtualBox — быстрая инструкция

Как исправить ошибку Crunchyroll P‑DASH‑114
Техподдержка

Как исправить ошибку Crunchyroll P‑DASH‑114