Как атомарно создать заблокированный файл в Linux?

Сценарий: у меня запущено много процессов, которым нужно получать файлы по сети. Если файл уже загружен, я хочу, чтобы он кэшировался на диске. Если другой процесс загружает файл, заблокируйте его, пока он не завершит загрузку.

Я пытался найти самый простой способ сделать это. Очевидный способ:

create file w/ an exclusive lock active on it only if it doesn't exist (O_CREAT | O_EXCL)
if file exists already:
   open file and acquire exclusive lock
else:
   download to newly created file
release lock

Эта система достигает вышеуказанных целей без (на первый взгляд) условий гонки.

К сожалению, я не смог найти документацию о том, как использовать open() и т. д. для создания файла, заблокированного в Linux. Если я разделю шаг создания на:

open w/ O_CREAT | O_EXCL
flock

между созданием и блокировкой теперь существует состояние гонки (не создающий процесс получает блокировку раньше, чем создатель).

Я понимаю, что мог бы использовать внешний файл блокировки для каждого файла (например, имя файла + '.lock), который я получаю перед попыткой создать имя файла, но это кажется... неэлегантным (и теперь мне нужно беспокоиться о том, как файлы, которые на самом деле имеют суффикс .lock!)

Есть ли способ атомарно создать и заблокировать его (как предлагает Windows) или метод внешнего файла блокировки в значительной степени является стандартным/требуемым?


person UsAaR33    schedule 27.06.2013    source источник


Ответы (3)


Раса в любом случае существует. Если файл может существовать или не существовать, вы должны проверить его существование, прежде чем пытаться его заблокировать. Но если файл является вашим мьютексом, вы не можете этого сделать, и пространство между «если файл уже существует» (false) и «загрузить во вновь созданный файл» не ограничено. Другой процесс может создать файл и начать загрузку до того, как начнется ваша загрузка, и вы уничтожите его.

В основном не используйте здесь блокировки fcntl, используйте существование самого файла. open() с O_CREAT и O_EXCL потерпит неудачу, если файл уже существует, сообщая вам, что кто-то другой добрался до него первым.

person Andy Ross    schedule 27.06.2013
comment
Я не уверен, как бы существовала гонка, если бы я мог атомарно создать заблокированный файл, только если файл не существует. (условие также атомарно). О_СОЗДАТЬ | O_EXCL — это атомарное создание, если файл не существует; Я просто хочу сделать это с замком. Другой процесс не может начать загрузку, так как он обнаружит, что файл существует. Наконец, я не могу использовать open отдельно, так как я должен заблокировать, пока загрузка не будет завершена; в моем решении я полагаюсь на блокировку (эксклюзивное чтение/запись). - person UsAaR33; 28.06.2013
comment
Вы правы, атомарное создание и блокировка файла решит вашу проблему. Но это не то, как работают блокировки fcntl, извините. Если вы хотите использовать блокировку fcntl, файл, который вы блокируете, должен существовать до того, как ваша программа предпримет какое-либо действие синхронизации. Найдите способ переместить создание за пределы этого if() в вашем псевдокоде, или же разработайте другой протокол синхронизации, который не использует загружаемый файл в качестве мьютекса. - person Andy Ross; 28.06.2013
comment
Понятно, поэтому ответ в том, что Linux не предлагает атомарные блокировки в стиле Windows с открытием. Я буду придерживаться метода lockfile. - person UsAaR33; 28.06.2013

Почему бы вам не использовать утилиту lockfile?

Примеры

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

...
lockfile important.lock
...
access_"important"_to_your_hearts_content
...
rm -f important.lock
...
person Jasio    schedule 02.09.2016

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

int fd = open(path, O_CREAT|O_RDWR|O_EXCL, mode);
if (fd == -1)
    {
    /* File already exists. */
    the_file_already_exists(fd);
    }
else
    {
    /* I just now created the file.  Now I'll lock it. */

    /* But first I'll deliberately create a race condition!! */
    deliberately_fork_another_process_that_handles_file(path);

    int code = flock(fd,LOCK_EX);
    if (code < 0)
        {
        perror("flock");
        exit(1);
        }

    /* I now have the exclusive lock.  I can write to the file at will --
    or CAN I??  See below. */
    write_to_the_file_at_will(fd);
    }

Очевидно, что в реальной жизни я бы никогда не стал намеренно создавать такое состояние гонки, но его эквивалент, безусловно, может произойти случайно в реальной системе. Этот другой процесс может, например, открыть файл для чтения, установить на него совместную блокировку и прочитать файл. Он увидит пустой файл. Это может означать, что выполняется операция записи, но может означать, что файл просто пуст, и это правильный и окончательный ответ.

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

Если пустые файлы разрешены, то вы находитесь в затруднительном положении, и у меня нет готового ответа на этот вопрос.

Моя проблема заключается в том, что когда файл создается впервые, я хочу записать в него какое-то значение по умолчанию, потому что я хочу «авто-инициализировать» новую систему без предварительного создания всех возможных файлов, которые ей могут понадобиться. Этот другой процесс, обрабатывающий файл, возможно, сам уже инициализировал его! Насколько мне известно, в то же время могли запуститься три других процесса, которые изменили значение. В этом случае я, конечно, не хочу "записывать в файл по своему желанию" после получения эксклюзивной блокировки, потому что я уничтожу все эти изменения.

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

the_file_already_exists(fd);

Возможно, суть всего этого обсуждения заключается в том, что каждый процесс, который каким-либо образом обрабатывает файл, должен проверять, не является ли он пустым, и вести себя соответствующим образом. Опять же, если разрешены пустые файлы, то я пока не могу придумать никакого гарантированного решения. Во всем этом не было бы необходимости, если бы существовал какой-то способ создать файл и заблокировать его как единую атомарную последовательность, но я не думаю, что есть какой-либо способ сделать это.

person Patrick Chkoreff    schedule 21.06.2018
comment
Один маленький момент: можно заблокировать сам родительский каталог. Просто откройте каталог только для чтения, как и любой другой файл, и получите эксклюзивную блокировку этого дескриптора. Пока все играют по правилам, теперь вы можете создавать и удалять файлы по своему усмотрению, пока у вас есть блокировка. Недостатком является то, что блокировка родительского каталога менее гранулярна, чем блокировка отдельных файлов. - person Patrick Chkoreff; 21.06.2018