Атомность `write (2)` в файле, открытом с флагом `O_APPEND` [дубликат]

Вот довольно краткий способ выполнения конкатенации:

>>> s = "hello world"
>>> ''.join(str(ord(c)) for c in s)
'10410110810811132119111114108100'

И своего рода забавная альтернатива:

>>> '%d'*len(s) % tuple(map(ord, s))
'10410110810811132119111114108100'
16
задан Community 23 May 2017 в 10:30
поделиться

4 ответа

man 2 write в моей системе прекрасно подсвечивает его:

Обратите внимание, что не все файловые системы соответствуют POSIX.

Вот цитата из недавнее обсуждение в списке рассылки ext4:

В настоящее время одновременные чтения / записи являются атомарными только по отдельным страницам, однако их нет в системном вызове , Это может привести к тому, что read() вернет данные, смешанные с несколькими различными записями, что, по-моему, не является хорошим подходом. Мы можем утверждать, что приложение, выполняющее это, нарушено, но на самом деле это то, что мы можем легко сделать на уровне файловой системы без существенных проблем с производительностью, поэтому мы можем быть последовательными. Кроме того, POSIX упоминает об этом, а файловая система XFS уже имеет эту функцию.

Это явное указание на то, что ext4 - для обозначения только одной современной файловой системы - t соответствуют POSIX.1-2008 в этом отношении.

10
ответ дан NPE 19 August 2018 в 00:23
поделиться
  • 1
    Хотя это меня очень огорчает, чем больше я вникаю в это, тем больше вы правы. – kmkaplan 19 May 2012 в 13:10

Изменить: Обновлено Aug 2017 с последними изменениями в поведении ОС.

Во-первых, O_APPEND или эквивалентный FILE_APPEND_DATA в Windows означает, что приращения максимальной длины файла (длина файла ») являются атомарными под параллельными авторами. Это гарантируется POSIX, и Linux, FreeBSD, OS X и Windows реализуют его правильно. Samba также реализует его правильно, NFS до v5 не делает, поскольку ему не хватает возможности форматирования каналов для атомарного добавления. Поэтому, если вы откроете свой файл только с помощью append-only, одновременные записи не будут разрываться друг относительно друга на любой основной ОС , если не задействована NFS.

Это ничего не говорит о том, читает ли когда-либо увидит разрывную запись, и на этом POSIX говорит следующее об атомарности read () и write () для обычных файлов:

Все следующие функции должны быть атомарными относительно друг друга в эффектах, указанных в POSIX.1-2008, когда они работают с обычными файлами или символическими ссылками ... [many functions] ... read () ... write () ... Если два потока каждый вызывает один из эти функции, каждый вызов должен либо увидеть все указанные эффекты другого вызова, либо ни один из них. [Источник]

и

Записи могут быть сериализованы относительно других чтений и записей. Если чтение () данных файла может быть доказано (каким-либо образом) после записи () данных, оно должно отражать эту запись (), даже если вызовы выполняются разными процессами. [Источник]

, но наоборот:

В этом томе POSIX.1-2008 не указано поведение одновременной записи в файл из нескольких процессов. Приложения должны использовать некоторую форму контроля параллелизма. [Источник]

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

Менее безопасным, но все же допускаемым толкованием может быть то, что чтение и запись только сериализации друг с другом между потоками внутри одного и того же процесса, а между процессами записи - сериализован только для чтения (т. е. последовательно выполняется согласование ввода / вывода между потоками в процессе, но между процессами i / o - это только получение-релиз).

Итак, как работают популярные ОС и файловые системы на этом? Как автор предлагаемого Boost.AFIO асинхронной файловой системы и библиотеки файлов i / o C ++, я решил написать эмпирический тестер. Следующие результаты для многих потоков в одном процессе.


Нет O_DIRECT / FILE_FLAG_NO_BUFFERING:

Microsoft Windows 10 с NTFS: update atomicity = 1 байт до 10.0 включительно. 10240, от 10.0.14393 не менее 1 Мб, возможно, бесконечно по спецификации POSIX.

Linux 4.2.6 с ext4: update atomicity = 1 байт

FreeBSD 10.2 с ZFS: обновление atomity = не менее 1 Мб, вероятно, бесконечно в соответствии с спецификацией POSIX.

O_DIRECT / FILE_FLAG_NO_BUFFERING:

Microsoft Windows 10 с NTFS: обновление атомарности = до 10.0.10240 до 1096 байты, только если выровнены по странице, иначе 512 байт, если FILE_FLAG_WRITE_THROUGH выключен, а еще 64 байта. Обратите внимание, что эта атомарность, вероятно, является отличительной чертой PCIe DMA, а не разработанной. С 10.0.14393, по крайней мере, 1 Мб, вероятно, бесконечно по спецификации POSIX.

Linux 4.2.6 с ext4: update atomicity = не менее 1 Мб, вероятно, бесконечно по спецификации POSIX. Обратите внимание, что ранее Linuxes с ext4 определенно не превышал 4096 байт, XFS, безусловно, использовала пользовательскую блокировку, но похоже, что последняя Linux окончательно устранила эту проблему в ext4.

FreeBSD 10.2 с ZFS: update atomicity = at менее 1 Мб, вероятно, бесконечно по спецификации POSIX.


Итак, во-первых, FreeBSD с ZFS и очень недавние Windows с NTFS соответствует POSIX. Очень недавний Linux с ext4 - это POSIX, соответствующий только O_DIRECT.

Вы можете увидеть исходные результаты эмпирического теста на странице https://github.com/ned14/afio/tree/master/programs/fs-probe . Обратите внимание, что мы тестируем разрывы смещений только на 512 байт-кратных, поэтому не могу сказать, будет ли частичное обновление 512-байтового сектора разрываться во время цикла чтения-изменения-записи.

7
ответ дан Niall Douglas 19 August 2018 в 00:23
поделиться
  • 1
    Вы имели в виду 512 байт, если FILE_FLAG_WRITE_THROUGH on ? Если нет, то почему это имеет смысл для этого флага, чтобы ухудшить ситуацию? – Mehrdad 31 December 2016 в 14:39
  • 2
    Причина, по которой писать, делает атомарность обновления меньшей, вероятно, потому, что Microsoft внедрила быстрый путь DMA и медленный путь, не поддерживающий DMA для обновлений, и когда запись выполняется, он использует медленный путь, который вообще не использует DMA, и выдает i / o возможно, используя опрос регистра. Учитывая, насколько удивительно медленный fsync и как это будет доминировать во всем другом коде с точки зрения стоимости, Microsoft не почувствовала необходимости быстрее писать путь к записи через код. В конце концов, только Microsoft знает наверняка, мой эмпирический тестер просто раскрывает атомарность, а не причины того, почему и почему нет. – Niall Douglas 1 January 2017 в 17:53

Вы неправильно интерпретируете первую часть указанной вами спецификации:

Либо дескриптор файла, либо поток называются «дескриптором» в описании открытого файла, к которому он относится; описание открытого файла может иметь несколько дескрипторов. [...] Вся активность приложения, влияющая на смещение файла на первом дескрипторе, приостанавливается до тех пор, пока он снова не станет активным дескриптором файла. [...] Ручки не обязательно должны быть в одном и том же процессе для применения этих правил.

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

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

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

6
ответ дан R.. 19 August 2018 в 00:23
поделиться
  • 1
    Случай для коротких записей обрабатывается в предоставленном образце. Теперь я перечитываю документ с учетом вашей интерпретации ... Я посмотрю, что он дает. – kmkaplan 18 May 2012 в 14:50
  • 2
    «Вся активность приложения, влияющая на смещение файла на первом дескрипторе, приостанавливается до тех пор, пока он снова не станет активным дескриптором файла». Как можно установить требование в приложение? Если он приостанавливает всю активность, влияющую на смещение файла, тогда в случае, когда один из дескрипторов является потоком, он никогда не сможет выполнить поиск [lf] seek (), чтобы «снова стать активным дескриптором файла». – kmkaplan 19 May 2012 в 13:05
  • 3
    Все, что говорится, это то, что вам не разрешено вызывать функции поиска на неактивном дескрипторе. Это активный дескриптор , который может потребоваться для вызова функции поиска, чтобы переключиться на новый дескриптор, и это совершенно законно. – R.. 19 May 2012 в 13:20
  • 4
    «Вам не разрешено вызывать функции поиска на неактивном дескрипторе» находится в противоречии с «Чтобы дескриптор стал активным дескриптором [...], приложение должно выполнить lseek () или fseek () [on it]" – kmkaplan 20 May 2012 в 09:15
  • 5
    Полагаю, я должен был сказать «когда другой дескриптор активен». – R.. 20 May 2012 в 14:56

Некоторое неверное истолкование того, что означает стандартные требования, происходит от использования процессов против потоков, и что это означает для ситуации с «ручкой», о которой вы говорите. В частности, вы пропустили эту часть:

Ручки могут быть созданы или уничтожены явным действием пользователя, не затрагивая описание открытого файла. Некоторые из способов их создания включают fcntl (), dup (), fdopen (), fileno () и fork(). Они могут быть уничтожены как минимум fclose (), close () и функциями exec. [...] Обратите внимание, что после fork () две ручки существуют там, где они существовали раньше.

из раздела спецификации POSIX, приведенного выше. Ссылка на «create [handles using] fork» не уточняется далее в этом разделе, но спецификация для fork() добавляет немного деталей:

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

Соответствующие биты здесь:

  • у ребенка есть копии дескрипторов файла родителя
  • копии ребенка относятся к той же «вещи», к которой родитель может получить доступ через указанный файл fds
  • ors и файл дескриптор ионов - это , а не то же самое; в частности, дескриптор файла является дескриптором в указанном выше смысле.

Это то, что первая цитата относится к тому, когда он говорит: «fork() создает [. ..] обрабатывает "- они создаются как копии , и, следовательно, с этой точки отсоединены и больше не обновляются в режиме блокировки.

В вашей примерной программе каждый дочерний процесс получает свою собственную копию, которая начинается в том же состоянии, но после акта копирования эти filedescriptors / handle стали независимыми экземплярами , и поэтому они пишут друг с другом. Это вполне приемлемо относительно стандарта, потому что write() только guarentees:

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

Это означает, что, хотя все они начинают писать с одинаковым смещением (потому что fd копия была инициализирована как таковая), они могут, даже в случае успеха, писать разные суммы (нет стандарта по стандарту, что запрос на запись в N байтах будет писать точно N, это может преуспеть для чего-либо 0 <= actual <= N), и из-за того, что упорядочение неупорядоченных операций не выполняется, вся приведенная выше примерная программа имеет неуказанные результаты. Даже если записана общая запрашиваемая сумма, все вышеприведенное значение говорит о том, что смещение файла увеличивается - оно не говорит, что оно атомарно (только один раз) увеличивается, и не говорит, что фактическая запись данных будет происходить в атомном режиме.

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

Использование O_APPEND исправляет это, потому что использование что еще раз - см. write() , делает:

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

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

Использование потоков частично изменило бы поведение, потому что потоки при создании не получали копии filedescriptors / handle, но работать с фактическим (общим). Нити не будут (обязательно) начинать писать с одного и того же смещения. Но вариант для частичной записи-успеха по-прежнему будет означать, что вы можете видеть чередование, как вы могли бы не видеть. Тем не менее, он может быть полностью совместим со стандартами.

Мораль: не считайте, что стандарт POSIX / UNIX является по умолчанию . Спецификации преднамеренно ослаблены в общем случае и требуют вы, как программист , быть ясными относительно ваших намерений.

8
ответ дан ʇsәɹoɈ 19 August 2018 в 00:23
поделиться
Другие вопросы по тегам:

Похожие вопросы: