Атомарная проверка наличия и создание файла блокировки

Я пишу программу синхронизирующую содержимое директорий на диске. Для того, чтобы исключить одновременный доступ нескольких экземпляров программы к одной директории, я создаю lock файл (или не обязательно файл, это может быть например директория). Если при обращении к директории программа видит там lock файл, то это означает, что с этой директорией уже кто-то работает и она не будет работать с этой директорией, а будет ждать пока lock файл не будет удалён.

Меня беспокоит такой вопрос: теоретически возможна ситуация, когда мы проверяем наличия файла на диске и видим, что его нет и создаём его сами, но в промежутке времени между проверкой и созданием файла он был создан другим экземпляром программы. В результате две программы уверены, что они владеют содержимым директории и будут изменять его одновременно не догадываясь об этом. Эта проблема описывается как Time-of-check to time-of-use race condition.

Как можно было бы исключить эту ситуацию? Или эта ситуация невозможна?

Использовать для решения функции специфичные для конкретных ОС нежелательно, так как общая папка может быть, например, сетевым диском WebDav или чем нибудь подобным. Желательно использовать функции из стандартной библиотеки C++.

Было найдено несколько потенциальных решений, но я не уверен, что эти операции являются атомарными. Возможные варианты решения:

  1. std::fopen с флагом "x".
  2. std::filesystem::create_directory
  3. std::filesystem::rename
  4. std::filesystem::copy_file

Эти решения являются атомарными функциями подходящими для создания файла блокировки? Или есть какое-то другое лучшее решение?


Ответы (1 шт):

Автор решения: maestro

Допустим, программа в данный момент хочет захватить блокировку. Выполняем std::FILE *f = fopen("lock", "wx");. Если файл уже существует (другой процесс уже захватил блокировку), то функция вернёт nullptr. Блокировка не удалась. Если же никто не захватил блокировку, то эта функция создаст файл блокировки.

Посмотрим на стандарт ISO/IEC 9899:201x, п. 7.21.5.3:

Opening a file with exclusive mode ('x' as the last character in the mode argument) fails if the file already exists or cannot be created. Otherwise, the file is created with exclusive (also known as non-shared) access to the extent that the underlying system supports exclusive access.

Перевод:

Открытие файла в исключительном режиме (символ 'x' завершает параметр 'mode') приводит к ошибке, если файл уже существует или не может быть создан. В противном случае, файл создаётся c исключительным (также известном как неразделяемым) режимом доступа в той мере, в какой нижележащая система поддерживает такой исключительный доступ.

Даже так сложно понять, что имеется в виду под "исключительностью". Понять, что речь идёт действительно о многопроцессном доступе, можно из предложения к стандарту WG14 N1339, по результатам которого исключительный доступ и был введён:

The C99 fopen() and freopen() functions are missing a mode character that will cause fopen() to fail rather than open a file that already exists. This is necessary to eliminate a time-of-creation to time-of-use race condition vulnerability.

Перевод:

Функции fopen() и freopen() языка C99 не содержат режима, при котором функция завершается с ошибкой, если файл уже существует. Такой режим необходим для решения уязвимости "time-of-creation to time-of-use race condition".

Получаем, что функция fopen с параметром "x" должна решить вашу задачу. Например, так:

#include <iostream>
#include <cstdio>

int main()
{

    std::FILE *f = std::fopen("lockfile", "wx");
    if (f)
    {
        std::cout << "Lock captured!" << std::endl;
    }
    else
    {
        std::cout << "Not captured, wait" << std::endl;
        do
        {
            f = std::fopen("lockfile", "wx");
        } while (!f);
        std::cout << "Lock captured!" << std::endl;
    }
    std::fclose(f);
    std::remove("lockfile");
    return 0;
}

Это было решение в стиле C. Оно полностью поддерживается языком C++, но всё же рассмотрим, как сделать подобное на C++.

Механизма исключительного доступа не было в C++ до C++23, и он был введён в стандарт предложением P2467R0. § 31.10.2.4, таблица 128 говорит, что параметр std::ios::noreplace идентичен параметру "x". Поэтому можно сделать так:

std::fstream f("filename", std::ios::out | std::ios::noreplace);
if (f.isOpen())
{
    // Файл блокировки создан
}
else
{
    // Файл уже существует
}
→ Ссылка