LINUX.ORG.RU

операция «открыть файл или создать его» — является ли атомарной и консистентной?

 ,


1

3

добрый день!

вопрос такой:

что касается операционных систем на ядре Linux — то для них операция "открыть файл (для записи) или создать его (если его нет)" является ли атомарной и консистентной?

в терминах C — например эта функция:

#include <stdio.h>
FILE *fopen(const char *path, const char *mode); // где mode = "w"

// Truncate file to zero length or create text file for
// writing. The stream is positioned at the beginning of the file. 

например, гипотетическая ситуация такая:

предположим, что сейчас файла «/run/my_super_file» — нет в каталоге «/run/».

но вдруг ни-с-того-ни-с-сего набежали тучи, и великое количество процессов вдруг-сразу-ОДНОВРЕМЕННО сделали вызов f = fopen("/run/my_super_file", "w");!

в этом случае:

1.[главный вопрос]: будут ли в итоге — все-ВСЕ эти процессы иметь дело с ровно-с-одним и тем же файлом? или кое-кто из процессов создаст свою собственную версию файла(?), и в этом случае очевидно предположить что неудачливый файл создастся без своего имени и сотрётся сразу после закрытия файлового дескриптора?

2. завершится ли эта команда у ВСЕХ этих процессов успешно? или кое-какие процессы отрапортируют ошибку (f == 0; errno == ...)?

3. будет ли различаться это поведение — в зависимости от различных файловых систем?

4. будут ли другие неожиданные последствия, которые я тут не упомянул?[этот вопрос снимаю, так как он слишком не конкретный :)]

спасибо за разъяснения! ^_^

Ответ на: комментарий от Deleted

man 2 open

этот абзац? :)

       O_CREAT
              If the file does not exist, it will be created.  The owner (user
              ID) of the file is set to the effective user ID of the  process.
              The  group  ownership  (group ID) is set either to the effective
              group ID of the process or to the group ID of the parent  direc‐
              tory  (depending  on  filesystem type and mount options, and the
              mode of the parent directory; see the  mount  options  bsdgroups
              and sysvgroups described in mount(8)).

              mode specifies the permissions to use in case a new file is cre‐
              ated.  This argument must be supplied when O_CREAT or  O_TMPFILE
              is specified in flags; if neither O_CREAT nor O_TMPFILE is spec‐
              ified, then mode is ignored.  The effective permissions are mod‐
              ified  by  the process's umask in the usual way: The permissions
              of the created file are (mode & ~umask).  Note  that  this  mode
              applies  only  to future accesses of the newly created file; the
              open() call that creates a read-only  file  may  well  return  a
              read/write file descriptor.
user_id_68054 ★★★★★ ()
Ответ на: комментарий от user_id_68054

Нет, он просто перепутал консоль с браузером.

i-rinat ★★★★★ ()

http://www.gnu.org/software/libc/manual/html_node/Opening-Streams.html говорит

Preliminary: | MT-Safe | AS-Unsafe heap lock | AC-Unsafe mem fd lock

То есть (смотрим http://www.gnu.org/software/libc/manual/html_node/POSIX-Safety-Concepts.html) безопасно для запуска в многопоточной среде (все процессы получат один и тот же файл) (вопрос 1), если смогут его открыть (вопрос 2. Смотри список возможных ошибок в man 2 open). Но эту функцию, например, небезопасно звать из хэндлеров сигналов. Preliminary значит, что эти свойства существуют в текущей версии glibc и в следующих версиях могут измениться.

Но попытка записи в файл открытый из нескольких процессов всё равно создаст ситуацию гонки.

kim-roader ★★ ()
Последнее исправление: kim-roader (всего исправлений: 1)
atomic_open: called on the last component of an open.  Using this optional
  	method the filesystem can look up, possibly create and open the file in
  	one atomic operation.  If it cannot perform this (e.g. the file type
  	turned out to be wrong) it may signal this by returning 1 instead of
	usual 0 or -ve .  This method is only called if the last component is
	negative or needs lookup.  Cached positive dentries are still handled by
	f_op->open().  If the file was created, the FILE_CREATED flag should be
	set in "opened".  In case of O_EXCL the method must only succeed if the
	file didn't exist and hence FILE_CREATED shall always be set on success.

Из Documentation/filesystems/vfs.txt

kike ()

Нижеследующее отностится к вызову open и косвенно применимо к функции fopen (которая очевидно сделает open с O_CREAT и O_TRUNC, не выпендриваясь, но в какой спецификации искать обещание всегда так делать, причём для разных реализаций libc — не представляю). Если нужна железная гарантия того, как будет вызываться open, вызывайте его сами и заворачивайте в ANSI stream через fdopen.

Также предлагаю ограничиться случаем файловой системы, «претендующей» на семантику POSIX — потому что VFS есть VFS, произвольный драйвер может почти произвольно извращаться; «/proc/self/fd/1», открытый в разных процессах, спокойно может ссылаться на разные файлы.

1. Всё будет хорошо, если никто не подкрадётся с unlink (в POSIX единственная ситуация, когда обычный файл есть, а имени у него нет, получается при удалении открытого файла; если один процесс его открыл с O_CREAT, другой удалил, третий открыл с тем же именем, у первого и третьего процесса файлы отличаются).

2. Ошибок, специфичных для данной ситуации, нет (если сами делаете open, как минимум надо делать повторную попытку для EINTR).

3. Даже среди VFS, не претендующих на POSIX и/или проблемных, не могу с ходу назвать контрпример. Искал бы таковой среди экзотических сетевых ФС (что-нибудь агрессивно кэширующее, способное отрапортовать успешный open до создания файла на удалённом узле, но неспособное объединить очередь запросов на запись post factum).

4. Неожиданные последствия будут, если вы не решите, как процессы должны писать в один файл, не превращая его в кашу. От тривиальных неожиданностей (внезапно поочерёдные fprintf из разных процессов не будут последовательно дописываться в конец файла, а будут делать кашу друг из друга) до нетривиальных (требование POSIX о том, что каждый write виден перекрывающему read либо как полностью случившийся, либо как не случавшийся, не касается fwrite и fread).

LeninGad ()
Ответ на: комментарий от i-rinat

Это универсальный ответ! man $N $вставить_что_попало!

Deleted ()
Ответ на: комментарий от LeninGad

спасиб!

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

ну это тож ясно, поэтому про запись в файл речь в этой теме не шла :-)

вообщем спасибо!

user_id_68054 ★★★★★ ()

нашёл где спросить... здесь программисты в красную книгу занесены

If O_CREAT and O_EXCL are set, open() shall fail if the file exists. The check for the existence of the file and the creation of the file if it does not exist shall be atomic with respect to other threads executing open() naming the same filename in the same directory with O_EXCL and O_CREAT set. If O_EXCL and O_CREAT are set, and path names a symbolic link, open() shall fail and set errno to [EEXIST], regardless of the contents of the symbolic link. If O_EXCL is set and O_CREAT is not set, the result is undefined.

http://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html

anonymous ()

Товарищ, тут написано про O_EXCL, а в вопросе чёрным по белому прямым русским языком подразумевается его отсутствие. Можно сделать правильный вывод, что в случае без O_EXCL стандартизаторы не видят даже возможности race condition, но этот вывод не делается из одного этого абзаца.

LeninGad ()
Ответ на: комментарий от LeninGad

что-нибудь агрессивно кэширующее, способное отрапортовать успешный open до создания файла на удалённом узле, но неспособное объединить очередь запросов на запись post factum

опасаюсь не агрессивного кэширования, а молодёжной моды на эффективное использование N-ядер процессора :-) ..

ну ладно. если встречу это где-то, то будем считать это багом..

user_id_68054 ★★★★★ ()
Последнее исправление: user_id_68054 (всего исправлений: 1)
Ответ на: комментарий от LeninGad

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

anonymous ()
Ответ на: комментарий от anonymous

узнаешь, что всё, что не оговорено в тз/требованиях, может быть реализовано как угодно. и такая реализация будет считаться корректной.

на самом деле — насрать с большой колокольни — на то что-и-где уточнено в документах TheOpenGroup..

лишь бы всё хорошо (хорошо с точки зрения разумного человека, а не с точки зрения нюансов стандарта) работало бы в ядре Linux. а что касается других операционных систем и их ядер — ды пусть они как хотят трактуют все эти стандарты.. :) я щитаю..

вот есть например там-всякие FreeBSD и OpenBSD .. тык им ещё Wayland и systemd (logind) нужно успеть освоить (а без этого — может произойти загибание быстрее чем кто-то вспомнит про очередной документ этого вашего POSIX).

user_id_68054 ★★★★★ ()
Последнее исправление: user_id_68054 (всего исправлений: 4)
Ответ на: комментарий от user_id_68054

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

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

если же провести небольшой экскурс в историю, то в ядрах как минимум 2.4.* ваши экзерсизы привели бы к утечке инодов — торвальдс не даст соврать. пофикшено ли это сейчас — не в курсе, я на это не рассчитываю.

кстати, крайний раз мне пришлось объяснять это молодому коллеге около 3 недель взад. поэтому я за включение этого топика в базовый курс си для чайников.

anonymous ()
Ответ на: комментарий от anonymous

тыг, погоди, товарищ аноним! значит ли это что ты считаешь что на «главный вопрос» — НЕльзя дать утвердительный ответ, в рамках этой темы?

я просто прочитал это твоё сообщение и не очень понял к чему ты клонишь...

говоришь что есть стандарт и что он описывает эту ситуацию туманно... но какой я должен сделать вывод-то теперь? (отвечая на «главный вопрос»).

мне теперь новые файлы нельзя создавать, через fopen(..., "w"), так как это может привести к ресурсовой утечке в ядре? :-)

или всё-таки можно? (но тогда как именно они будут создаваться в условиях жестокой конкуренции процессов?)

это говорит лишь о том, молодой человек, что вы, во-первых, не умеете читать документацию, ...

не умею, да, признаюсь :-) .. здесь вы меня подловили :-D

во-вторых, рассчитываете на запуск вашего приложения исключительно на локалхосте.

ну как минимум — уж точно не расчитываю на *BSD и на Windows. потому что *BSD я давно уже ни где не видал (кроме страшных снов), а Windows какая-то слишком странноватая для меня, когда дело доходит там до IPC и до файлов..

кстати, крайний раз мне пришлось объяснять это молодому коллеге около 3 недель взад. поэтому я за включение этого топика в базовый курс си для чайников.

одобряю! :)

а ещё лучше — это — вообще включить этот топик в «базовый курс для тех кто впервые увидел ЭВМ» :-)

потому что — чёрт-же-возьми — реально(!) всем нужно знать как работает файловая система, даже если ты не программист..

user_id_68054 ★★★★★ ()
Последнее исправление: user_id_68054 (всего исправлений: 5)
Ответ на: комментарий от user_id_68054

на «главный вопрос» — НЕльзя дать утвердительный ответ, в рамках этой темы?

если ты настаиваешь, я дам тебе несколько хамоватый, но технически абсолютно корректный ответ: случиться может всё что угодно.

описывает эту ситуацию туманно...

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

нельзя создавать, через fopen(..., «w»)

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

anonymous ()
Ответ на: комментарий от user_id_68054

чёрт-же-возьми — реально(!) всем нужно знать как работает файловая система

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

anonymous ()
Ответ на: комментарий от anonymous

ну вообщем-то, да.. хочу сказать что на умную мысль ты меня навёл успешно (я про O_EXCL) :-)

user_id_68054 ★★★★★ ()
Ответ на: комментарий от user_id_68054

это в награду за то, что ты перед тем как насрать в /development задал себе правильные вопросы и попытался в них разобраться.

anonymous ()
Ответ на: комментарий от anonymous

писать на питоне

причём тут язык-то? :) я же привёл C только лишь потому что все его знают.

ну в терминах python будет так:

with open('/run/my_super_file', 'wb') as fd:
    # ...
    # ...
    # ...

а суть таже самая! :-)

[ещё конечно в python есть модуль «os» и его «os.open(..)»]

user_id_68054 ★★★★★ ()
Ответ на: комментарий от user_id_68054

питон я привёл исключительно как пример языка без документации

anonymous ()
Ответ на: комментарий от user_id_68054

Советую быть поосторожнее с этой умной мыслью. Если по смыслу вам O_EXCL не нужен (т.е. нужно именно открыть один и тот же файл во всех процессах), вы сможете имитировать отсутствие O_EXCL (т.е. обычный creat()) только чередованием open с EXCL|CREAT и open без EXCL|CREAT, в конце концов дождавшись успеха от одного из них. А это как раз верный способ словить проблем на пустом месте (с точки зрения POSIX всё хорошо, а вот с точки зрения реализаций не всегда).

Вообще здешний анонимус (или их несколько?) прав по поводу документации, по поводу необходимости либо читать стандарт, либо вкуривать каждое изменение kernel или glibc; неправ по поводу «может случиться что угодно» в отношении одновременного open без O_EXCL (и в отношении одновременного fopen, если рассматривать POSIX fopen, а не ANSI/ISO fopen, т.е. если libc претендует на поддержку POSIX); заблуждается по поводу утечки inode в ядрах 2.4 при вашем сценарии использования (ну или не смог понять описание сценария).

Мы знаем из официального источника, что все функции создания файлов из System Interfaces атомарны (не привожу точную ссылку только для того, чтобы если разгорится дискуссия, знать, чьи именно контраргументы стоит проверять при неочевидной валидности). Именно поэтому комментарий к O_EXCL надо понимать как гарантию отсутствия «окна» между проверкой существования файла и созданием несуществующего (иначе говоря, работающий O_EXCL не подвержен TOCTOU и не может открыть файл, созданный другим процессом или нитью между проверкой и «собственно открытием»). То, что к моменту вызова POSIX compliant open/creat/fopen файл не может быть в «полусозданном» состоянии, а либо уже создан, либо ещё не создан, специально оговаривать в описании open не имеет смысла.

LeninGad ()
Ответ на: комментарий от LeninGad

Было дело, на чистых сях, я пилил тестовый бинарник.

Бинарник делал вот что: открывал (создавал если не существует) файл на запись с эксклюзивной блокировкой и дописывал в конец файла некий struct маленького размера.

Так вот при запуске этого бинарика вот таким макаром:

find ./ -name "*.txt" -exec ./binarik {} \;

при большом количестве вызовов, где-то на 30-80 найденных файлов бинарик внешне не фейлился, но при просмотре результирующего файла - были потеряны эти over-30-80 записей, т.е. они тупо не добавлялись в индекс-файл, а «успевали» добавиться только первые.

Работало это только если запускать небольшими пачками.

Как это объяснить? Или ЧЯДНТ?

deep-purple ★★★★★ ()
Ответ на: комментарий от LeninGad

несём чушь с умным видом? ну-ну

функции создания файлов из System Interfaces атомарны (не привожу точную ссылку только для того

почему так скромно? раздел 2.9.7. но. во-первых, он касается потоков внутри одного процесса, во-вторых, гарантирует только то, что поток увидит или все изменения (путь существует + файл полностью создан) или никаких (путь не существует + файл не создан). для реализации этого требования open объявлен обязательным cancellation point (раздел 2.9.5).

anonymous ()
Ответ на: комментарий от deep-purple

но ведь find -exec НЕ станет запускать утилиту в случае когда экспериментальный файлы НЕ существует? :)

(исключение может быть случай когда сразу после проверки, экспериментальный файл <кто-то-другой> удалил\переименовал)

user_id_68054 ★★★★★ ()
Ответ на: комментарий от LeninGad

вы сможете имитировать отсутствие O_EXCL (т.е. обычный creat()) только чередованием open с EXCL|CREAT и open без EXCL|CREAT, в конце концов дождавшись успеха от одного из них. А это как раз верный способ словить проблем на пустом месте

да, правда в том что, если бы мне точно знать, что O_CREAT|O_TRUNC (без O_EXCL) — работает именно атомарно-и-консистентно для множества конкурентных процессов, то это позволило бы избежать различные костыли.

потому что ведь костыль можно придумать и другой, не обязательно именно O_EXCL .

костыль может быть например такой (без O_EXCL):

1. сначало открывать каталог (например «/run/») как файловый-дескриптор.

2. затем делать эксклюзивную блокировку этого каталога (по его файловому-дескриптору).

3. затем f = open("/run/my_super_file" , O_CREAT|O_TRUNC );

4. затем (перед началом работы с f) освобождать эксклюзивную блокировку каталога.

5. непосредственная работа с файловым дескриптором f...

6. ...

вот только можно было бы избежать использования [1],[2],[4] , если бы точно знать что и без этого всё должно нормально работать..

user_id_68054 ★★★★★ ()
Последнее исправление: user_id_68054 (всего исправлений: 4)
Ответ на: комментарий от user_id_68054

Экспериментальный файл создавала и дописывала сама утилита которую дергал финдовый экзек.

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

Экспериментальный файл создавала и дописывала сама утилита которую дергал финдовый экзек.

а понял!

например file -exec нашла файл «/run/my_super_file» , а утилита создала файл «/run/my_super_file.meta»

да?

user_id_68054 ★★★★★ ()
Ответ на: комментарий от user_id_68054

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

Однако, если финд много раз и часто дергал эту утилиту, то хоть она и не фейлилась, но «успевала» добавлять только некоторую начальную пачку данных. А вот если финда натравливал не рекурсивно, то она нормально хавала все данные. Порог фейла примерно был на 30-80 вызовов утилиты.

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

deep-purple ★★★★★ ()
Ответ на: комментарий от deep-purple

Эксклюзивная блокировка — это про mandatory lock или про что? (ну, вряд ли про O_EXCL, раз уж результирующий файл дописывается).

Большое количество вызовов — это про несколько параллельных find'ов, или про один find, который много всего нашёл?

Дописывание в конец файла — через open с O_APPEND, через fopen(...,«a»), или через lseek(..SEEK_END)? А затем через fwrite или через write? А размер записи какой? (это всё интересно для случая, когда в файл могут дописывать параллельно; с одиночным экземпляром find такого, надо сказать, не будет — он дожидается завершения одного процесса, прежде чем запустить следующий).

Судя по тому, что вам потребовались блокировки, предполагалось параллельное дописывание в файл. По POSIX это более-менее предсказуемо работает только с open(O_APPEND), но в линуксе даже с этим была проблема, исправленная совсем недавно (3.14).

LeninGad ()
Ответ на: комментарий от anonymous

раздел 2.9.7. но. во-первых, он касается потоков внутри одного процесса

а вроде бы же в ядре Linux поток (точнее: нить, Thread) — это всего лишь частный случай от процесса?

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

user_id_68054 ★★★★★ ()
Ответ на: комментарий от user_id_68054

а вроде бы же в ядре Linux поток (точнее: нить, Thread) — это всего лишь частный случай от процесса?

Во-первых, это особенность реализации, во-вторых, это уже не актуальная особенность устаревшей реализации.

Но в отношении атомарности создания файла я имел в виду другое место в другом документе, причём, по-моему, оно там было ещё до стандартизации pthreads (впрочем, за это не поручусь) и к pthreads оно никак не относится.

LeninGad ()
Ответ на: комментарий от LeninGad

Объем дописываемых за один раз данных был маленький - структ в два поля: путь до файла и имя очищенное от расширения.

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

deep-purple ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.