0Pricing
Linux Command Line & Bash Scripting Mastery · Урок

Безопасные временные файлы и каталоги блокировок

Используйте mktemp и flock для создания временных ресурсов без состязаний и предотвращения одновременного запуска скриптов

«Безопасные временные файлы и каталоги блокировок» — бесплатный урок Linux Command Line & Bash Scripting Mastery на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Linux Command Line & Bash Scripting Mastery, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Linux Command Line & Bash Scripting Mastery содержит 4 уроков всего.

Почему временные файлы представляют угрозу безопасности

Сценариям Bash часто требуется временное хранилище — для промежуточных результатов, маркеров блокировки и областей подготовки. Но неосторожное создание временных файлов открывает серьёзные уязвимости.

  • Состояния гонки: другой процесс может предсказать имя файла и создать его первым, перенаправив Вашу запись.
  • Атаки через символические ссылки: злоумышленник создаёт символическую ссылку по ожидаемому пути, указывающую на важный файл, например /etc/passwd.
  • Оставшиеся файлы: если сценарий аварийно завершится, временные файлы будут накапливаться и могут раскрыть конфиденциальные данные.

Два основных инструмента, устраняющих эти проблемы, — mktemp и flock. В этом уроке Вы узнаете, как безопасно и надёжно использовать оба инструмента.

Безопасное создание временных файлов с помощью mktemp

mktemp создаёт временный файл со случайным непредсказуемым именем и возвращает его путь. Файл создаётся атомарно, поэтому другой процесс не сможет первым занять это имя.

  • Синтаксис: mktemp [TEMPLATE] — шаблон должен заканчиваться как минимум тремя символами X.
  • Каждый символ X заменяется случайным символом, в результате чего получается уникальное имя, например /tmp/script.aB3kQz.
  • Файл автоматически создаётся с правами 0600 (читать его может только владелец).

Всегда сразу сохраняйте возвращённый путь в переменной, чтобы позднее обращаться к файлу и удалять его.

#!/usr/bin/env bash
set -euo pipefail

# Create a secure temp file
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
echo "Temp file created at: $TMPFILE"

# Write data to it
echo "some intermediate result" > "$TMPFILE"

# Read it back
cat "$TMPFILE"

# Clean up
rm -f "$TMPFILE"

Всегда выполняйте очистку с помощью trap

Если сценарий неожиданно завершится — из-за ошибки, сигнала или срабатывания set -e, — временные файлы останутся, если не зарегистрировать обработчик очистки.

Встроенная команда trap выполняет команду, когда оболочка получает сигнал или завершается. Канонический шаблон очистки временного файла:

  • Зарегистрируйте ловушку сразу после создания временного файла.
  • Установите ловушку для EXIT, чтобы очистка выполнялась и при штатном, и при аварийном завершении.
  • Также установите ловушки для INT и TERM, если сценарий работает долго или взаимодействует с пользователем.

Это гарантирует отсутствие бесхозных файлов, даже если сценарий будет принудительно завершён в процессе выполнения.

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/report.XXXXXX)

# Register cleanup before doing any real work
cleanup() {
    rm -f "$TMPFILE"
    echo "Cleaned up $TMPFILE" >&2
}
trap cleanup EXIT

# Do work — even if this fails, cleanup() will run
echo "Processing..." > "$TMPFILE"
grep "result" "$TMPFILE" || true

echo "Done. File will be removed on exit."

Создание временных каталогов с помощью mktemp -d

Иногда Вам нужен целый каталог для размещения нескольких файлов — например, для создания архива или распаковки tar-архива перед обработкой. Используйте mktemp -d, чтобы создать безопасный временный каталог.

  • Каталог создаётся с разрешениями 0700 (доступ только владельцу).
  • Удаляйте его с помощью rm -rf в обработчике trap — внимательно следите за тем, чтобы удалять только переменную, а не жёстко заданный путь.
  • Используйте двойные кавычки и перед вызовом rm -rf проверяйте, что переменная не пуста, в качестве дополнительной меры безопасности.
#!/usr/bin/env bash
set -euo pipefail

TMPDIR=$(mktemp -d /tmp/extract.XXXXXX)

cleanup() {
    # Guard: only rm if variable is set and non-empty
    [[ -n "${TMPDIR:-}" ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT

echo "Working in $TMPDIR"

# Simulate staging files
echo "file one" > "$TMPDIR/part1.txt"
echo "file two" > "$TMPDIR/part2.txt"

ls "$TMPDIR"
echo "All done."

Проблема одновременного запуска скриптов

Задания cron, таймеры systemd и скрипты, запущенные вручную, могут легко одновременно запустить несколько экземпляров одного и того же скрипта. Это приводит к следующему:

  • Повторная обработка: одни и те же записи базы данных или файлы обрабатываются дважды.
  • Повреждение результата: два экземпляра одновременно записывают данные в один и тот же выходной файл.
  • Взаимные блокировки или частично изменённое состояние: оба экземпляра изменяют общие ресурсы в перемешанном непредсказуемом порядке.

Традиционным решением было записать PID-файл и проверить его при запуске, но между проверкой и записью возникает окно состязания. Правильное современное решение — flock, использующий рекомендательный механизм блокировок ядра для гарантированной блокировки без состязаний.

Блокировка с помощью flock: однострочный шаблон

flock устанавливает рекомендательную блокировку дескриптора файла перед выполнением команды. Самый простой способ использования — обернуть весь скрипт в командной строке:

flock -n /var/lock/myscript.lock bash myscript.sh

  • -n (неблокирующий режим): немедленно завершает работу со статусом 1, если блокировка уже установлена, вместо ожидания.
  • Без -n команда flock блокируется до освобождения блокировки — это удобно для постановки в очередь.
  • Сам файл блокировки является лишь маркером; его содержимое не имеет значения. Безопасно сохранять его между запусками.
  • Когда процесс, удерживающий блокировку, завершает работу, ядро автоматически освобождает блокировку — ручная очистка не требуется.
#!/usr/bin/env bash
# launcher.sh — prevents concurrent runs of worker.sh
set -euo pipefail

LOCKFILE="/tmp/myworker.lock"

if ! flock -n "$LOCKFILE" bash -c 'echo "Running worker..."; sleep 2; echo "Done."'; then
    echo "Another instance is already running. Exiting." >&2
    exit 1
fi

Использование flock внутри скрипта с дескриптором файла

Чтобы установить блокировку внутри скрипта, а не оборачивать его снаружи, используйте exec для открытия дескриптора файла, а затем вызовите flock для этого дескриптора. Это стандартный шаблон для рабочих скриптов.

  • exec 200>"$LOCKFILE" открывает файл для записи через дескриптор 200 (и создаёт его при необходимости).
  • flock -n 200 пытается установить неблокирующую блокировку дескриптора 200.
  • Поскольку блокировка связана с дескриптором файла, а не с именем файла, она автоматически освобождается при завершении процесса оболочки.
  • Номера дескрипторов 200–299 обычно используют, чтобы избежать конфликтов с stdin/stdout/stderr.
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myjob.lock"

# Open lock file on FD 200
exec 200>"$LOCKFILE"

# Attempt non-blocking lock
if ! flock -n 200; then
    echo "ERROR: Another instance of this script is running." >&2
    exit 1
fi

echo "Lock acquired. Starting work..."
sleep 1
echo "Work complete. Lock will be released on exit."

Объединение mktemp и flock в одном скрипте

Настоящим защитным скриптам нужны оба механизма: блокировка для предотвращения одновременных запусков и безопасные временные файлы для промежуточных данных. Ниже приведён полный шаблон, объединяющий оба приёма:

  • Сначала установите блокировку — до создания временных файлов, — чтобы работу вообще выполнял только один экземпляр.
  • Создавайте временные ресурсы после подтверждения блокировки.
  • Сразу после создания временных ресурсов зарегистрируйте trap, чтобы очистка гарантированно выполнялась независимо от способа завершения скрипта.
  • Файл блокировки никогда не помещайте во временный каталог — он должен сохраняться между запусками, чтобы flock мог обращаться к нему.
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/report_builder.lock"
exec 200>"$LOCKFILE"

if ! flock -n 200; then
    echo "Already running — aborting." >&2
    exit 1
fi

# Now safe to create temp resources
TMPDIR=$(mktemp -d /tmp/report.XXXXXX)
TMPLOG=$(mktemp /tmp/report_log.XXXXXX)

cleanup() {
    rm -rf "${TMPDIR:-}"
    rm -f  "${TMPLOG:-}"
}
trap cleanup EXIT

echo "Building report in $TMPDIR" | tee "$TMPLOG"
echo "Step 1 complete"             >> "$TMPLOG"
cat "$TMPLOG"

Блокировка каталогами как альтернативный механизм

В системах, где flock недоступен (например, во встраиваемых системах или сетевых файловых системах, таких как NFS), вместо него можно использовать каталоги блокировки. В системах POSIX команда mkdir выполняется атомарно: она завершается успешно, только если каталога ещё не существует.

  • Создайте каталог блокировки с помощью mkdir /tmp/myscript.lock.d — если другой экземпляр уже создал его, команда mkdir немедленно завершится с ошибкой.
  • Храните внутри каталога метаданные (например, PID) для диагностики.
  • Всегда удаляйте каталог в обработчике trap для EXIT.
  • Важно: в отличие от flock, блокировка каталогом не освобождается автоматически, если процесс завершён с помощью -9 или компьютер перезагружается. Добавьте проверку устаревшей блокировки.
#!/usr/bin/env bash
set -euo pipefail

LOCKDIR="/tmp/myscript.lock.d"

# Atomic mkdir — fails if directory already exists
if ! mkdir "$LOCKDIR" 2>/dev/null; then
    # Check if the holding PID is still alive
    HOLDER_PID=$(cat "$LOCKDIR/pid" 2>/dev/null || echo "")
    if [[ -n "$HOLDER_PID" ]] && kill -0 "$HOLDER_PID" 2>/dev/null; then
        echo "Locked by PID $HOLDER_PID. Exiting." >&2
        exit 1
    else
        echo "Stale lock detected. Removing and continuing." >&2
        rm -rf "$LOCKDIR"
        mkdir "$LOCKDIR"
    fi
fi

echo "$$" > "$LOCKDIR/pid"
trap 'rm -rf "$LOCKDIR"' EXIT

echo "Lock acquired via directory. Running..."
sleep 1
echo "Done."

Ожидание с ограничением времени с помощью flock

Иногда нужно дождаться блокировки, а не немедленно завершить работу с ошибкой, но ждать бесконечно тоже нельзя. flock поддерживает ограничение времени с помощью флага -w.

  • flock -w 10 200 ждёт блокировку не более 10 секунд, а затем завершается со статусом 1, если она всё ещё недоступна.
  • Это идеально подходит для скриптов, которые должны встать в очередь за короткоживущим предшествующим процессом, но завершить ожидание, если тот завис.
  • Сочетайте -w с содержательным сообщением об ошибке, включающим контекст — путь к файлу блокировки и длительность ожидания, — чтобы операторы могли быстро диагностировать зависания.
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/data_sync.lock"
TIMEOUT=15

exec 200>"$LOCKFILE"

echo "Waiting up to ${TIMEOUT}s for lock on $LOCKFILE..."

if ! flock -w "$TIMEOUT" 200; then
    echo "ERROR: Could not acquire lock after ${TIMEOUT}s." \
         "Another instance may be hung." >&2
    exit 1
fi

echo "Lock acquired. Syncing data..."
sleep 1
echo "Sync complete."

Контрольный список защиты: безопасные временные ресурсы

Перед выпуском любого скрипта, использующего временные файлы или блокировки, пройдите этот контрольный список:

  • Используйте mktemp, а не жёстко заданные пути — путь /tmp/myapp.tmp предсказуем и может быть использован злоумышленником.
  • Сразу сохраните путь — выполните TMPFILE=$(mktemp ...) до любой другой команды.
  • Сразу после создания зарегистрируйте trap cleanup EXIT — не откладывайте это до конца скрипта.
  • Заключайте все обращения к переменным в двойные кавычки — используйте rm -f "$TMPFILE", а не rm -f $TMPFILE.
  • Предпочитайте flock PID-файлам — блокировкой управляет ядро, и она автоматически освобождается при аварийном завершении.
  • По умолчанию используйте неблокирующий режим -n — молчаливое ожидание блокировок скрывает проблемы с производительностью.
  • Размещайте файл блокировки вне временного каталога — тогда он сохранится после очистки в обработчике trap.
  • Проверьте поведение очистки — запустите скрипт и выполните для него kill -9 во время работы; убедитесь, что не осталось лишних файлов (для скриптов на основе flock; блокировки каталогами требуют дополнительных мер).

Проверка знаний: поведение флагов flock

Задание cron запускается каждую минуту и обрабатывает общий файл. Вы хотите, чтобы любой новый запуск немедленно завершался с ошибкой, если предыдущий запуск ещё активен, не дожидаясь его завершения. Какой вызов flock правильно это реализует?

Итоги: безопасные временные файлы и каталоги блокировки

В этом уроке Вы изучили два основных инструмента безопасного управления ресурсами в Bash:

  • mktemp создаёт непредсказуемые временные файлы (0600) и каталоги (0700) с безопасными разрешениями, устраняя состязания и атаки через символические ссылки, характерные для жёстко заданных путей.
  • trap cleanup EXIT гарантирует удаление временных файлов при любом завершении — обычном, вызванном ошибкой или сигналом, — если зарегистрировать его сразу после создания.
  • flock предоставляет рекомендательную блокировку, обеспечиваемую ядром: используйте -n для немедленного завершения при конфликте, -w N для ожидания с ограничением времени и шаблон exec 200>file для блокировки внутри скрипта, которую ядро автоматически освобождает при завершении процесса.
  • Каталоги блокировки (mkdir) служат переносимым резервным вариантом в средах, где flock недоступен, но требуют явной проверки устаревших блокировок.
  • Всегда храните файл блокировки вне временного каталога и заключайте каждое используемое при очистке обращение к переменной в двойные кавычки.

Сочетание mktemp + flock + trap позволяет создавать скрипты, защищённые от одновременных запусков, непредсказуемых сбоев и злонамеренных манипуляций с файловой системой.

Часто задаваемые вопросы

Урок «Безопасные временные файлы и каталоги блокировок» бесплатный?

Да — полный текст урока «Безопасные временные файлы и каталоги блокировок» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Linux Command Line & Bash Scripting Mastery, подпишись на CoddyKit PRO. Курс Linux Command Line & Bash Scripting Mastery содержит 4 уроков всего.

Чему я научусь в уроке «Безопасные временные файлы и каталоги блокировок»?

Используйте mktemp и flock для создания временных ресурсов без состязаний и предотвращения одновременного запуска скриптов Ты практикуешь Linux Command Line & Bash Scripting Mastery с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Linux Command Line & Bash Scripting Mastery?

Предыдущий опыт не требуется. Linux Command Line & Bash Scripting Mastery на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Безопасные временные файлы и каталоги блокировок»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Linux Command Line & Bash Scripting Mastery?

Да. Каждый урок Linux Command Line & Bash Scripting Mastery включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Строгий режим с set -euo pipefail
  2. Обработчики trap для очистки и сигналов
  3. Безопасные временные файлы и каталоги блокировок
  4. Идемпотентные скрипты и логика повторных попыток с задержкой
← Назад к Linux Command Line & Bash Scripting Mastery