Атомность `write (2)` к локальной файловой системе

Очевидно, POSIX утверждает, что

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

а такж

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

Я понимаю, что когда первый процесс выдаетwrite(handle, data1, size1) и второй процесс выдаетwrite(handle, data2, size2), запись может происходить в любом порядке, кромеdata1 а такжеdata2 долже будь и нетронутым, и непрерывным.

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

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
die(char *s)
{
  perror(s);
  abort();
}

main()
{
  unsigned char buffer[3];
  char *filename = "/tmp/atomic-write.log";
  int fd, i, j;
  pid_t pid;
  unlink(filename);
  /* XXX Adding O_APPEND to the flags cures it. Why? */
  fd = open(filename, O_CREAT|O_WRONLY/*|O_APPEND*/, 0644);
  if (fd < 0)
    die("open failed");
  for (i = 0; i < 10; i++) {
    pid = fork();
    if (pid < 0)
      die("fork failed");
    else if (! pid) {
      j = 3 + i % (sizeof(buffer) - 2);
      memset(buffer, i % 26 + 'A', sizeof(buffer));
      buffer[0] = '-';
      buffer[j - 1] = '\n';
      for (i = 0; i < 1000; i++)
        if (write(fd, buffer, j) != j)
          die("write failed");
      exit(0);
    }
  }
  while (wait(NULL) != -1)
    /* NOOP */;
  exit(0);
}

Я попытался запустить это на Linux и Mac OS X 10.7.4 и использоватьgrep -a '^[^-]\|^..*-' /tmp/atomic-write.log показывает, что некоторые записи не являются непрерывными или перекрываются (Linux) или просто повреждены (Mac OS X).

Добавление флагаO_APPEND вopen(2) call решает эту проблему. Приятно, но я не понимаю почему. POSIX говорит

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

но это не проблема здесь. Моя программа-пример никогда не делаетlseek(2) но с одинаковым описанием файла и таким же смещением файла.

Я уже читал похожие вопросы о Stackoverflow, но они все еще не полностью отвечают на мой вопрос.

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

Как программно определить, является ли системный вызов «write» атомарным для определенного файла? Говорит, чт

Thewrite @ call, как определено в POSIX, вообще нет гарантии атомарности.

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

Можете ли вы объяснить это поведение?

Ответы на вопрос(4)

man 2 write в моей системе это хорошо подводит итог:

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

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

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

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

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

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

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

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

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

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

из раздела спецификации POSIX, который вы цитируете выше. Ссылка на "создать [обрабатывает с помощью]fork "не рассматривается далее в этом разделе, но спецификация дляfork() добавляет немного деталей:

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

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

у ребенка есть Копии файловых дескрипторов родителя копии ребенка относятся к той же «вещи», к которой родитель может получить доступ через указанный fdsфайл Неописуемой ПРС и файл Неописуемой Ионы являютсян тоже самое; в частности, файловый дескриптор представляет собойсправитьс в вышеприведенном смысле.

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

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

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

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

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

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

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

которая представляет собой сериализованное поведение «до» / «без вмешательства», которое вы ищете.

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

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

Редактировать Обновлен в августе 2017 года с последними изменениями в поведении ОС.

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

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

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

а такж

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

но наоборот:

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

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

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

Так как на этом работают популярные ОС и файловые системы? Как предложил автор Boost.AFIO Асинхронная файловая система и библиотека ввода-вывода C ++, я решил написать эмпирический тестер. Результаты приведены для многих потоков в одном процессе.

Нет O_DIRECT / FILE_FLAG_NO_BUFFERING:

Microsoft Windows 10 с NTFS: атомарность обновления = 1 байт до 10.0.10240 включительно, начиная с 10.0.14393, по крайней мере 1 МБ, вероятно, бесконечно согласно спецификации POSIX.

Linux 4.2.6 с ext4: атомарность обновления = 1 байт

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

O_DIRECT / FILE_FLAG_NO_BUFFERING:

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

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

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

Итак, в целом, FreeBSD с ZFS и совсем недавно Windows с NTFS соответствуют POSIX. Совсем недавно Linux с ext4 соответствовал POSIX только O_DIRECT.

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

ВАШ ОТВЕТ НА ВОПРОС