Атомность `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) но использовать одно и то же описание файла и, таким образом, такое же смещение файла.

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

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

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

write У вызова, как определено в POSIX, нет никакой гарантии атомарности вообще.

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

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

4 ответа

Решение

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

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

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

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

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

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

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

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

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

а также

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

но наоборот:

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

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

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

Итак, как на этом работают популярные ОС и файловые системы? Поскольку автор предложил 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: атомарность обновления = не менее 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-probe. Обратите внимание, что мы тестируем разрывы только на 512 байтов, поэтому я не могу сказать, разорвется ли частичное обновление сектора 512 байт во время цикла чтения-изменения-записи.

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

Дескрипторы могут быть созданы или уничтожены явным действием пользователя, не влияя на описание открытого открытого файла. Некоторые из способов их создания включают 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 ограничивает по умолчанию. Спецификации преднамеренно смягчены в общем случае и требуют, чтобы вы, как программист, четко заявляли о своих намерениях.

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

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

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

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

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

Другие вопросы по тегам