`write(2)`对本地文件系统的原子性

时间:2012-05-18 10:23:59

标签: c file-io posix multiprocessing

显然POSIX声明

  

文件描述符或流被称为“句柄”   它引用的打开文件描述;一个打开的文件描述   可能有几个手柄。 [...]应用程序的所有活动   影响第一个手柄上的文件偏移应暂停   直到它再次成为活动文件句柄。 [...]手柄需要   这些规则不适用于同一过程。    - POSIX.1-2008

  

如果两个线程分别调用[write()函数],则每次调用都应该   或者看到另一个调用的所有指定效果,或者没有   他们    - POSIX.1-2008

我对此的理解是,当第一个流程发布时 write(handle, data1, size1)和第二个流程问题 write(handle, data2, size2),写入可以按任何顺序发生但是 data1data2 必须既质朴又连续。

但运行以下代码会给我带来意想不到的结果。

#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)可修复此问题 问题。很好,但我不明白为什么。 POSIX说

  

O_APPEND      如果设置,则文件偏移量应在每次写入之前设置为文件的末尾。

但这不是问题所在。我的示例程序永远不会 lseek(2)但共享相同的文件描述,因此共享相同的文件 偏移量。

我已经在Stackoverflow上阅读了类似的问题,但它们仍然存在 不完全回答我的问题。

Atomic write on file from two process没有具体说明 解决进程共享相同文件描述的情况 (与同一文件相对)。

How does one programmatically determine if “write” system call is atomic on a particular file?

  

POSIX中定义的write调用完全没有原子性保证。

但是cited above确实有一些。更重要的是, 尽管似乎O_APPEND似乎触发了这种原子性保证 对我而言,即使没有O_APPEND,这种保证也应该存在。

你能进一步解释一下这种行为吗?

4 个答案:

答案 0 :(得分:11)

我系统上的

man 2 write总结得很好:

  

请注意,并非所有文件系统都符合POSIX。

以下是ext4邮件列表中最近discussion的引用:

  
    

目前并发读/写只是单个页面的原子,     但是不在系统调用上。这可能会导致read()返回数据     从几个不同的写入混合,我不认为这是好的     做法。我们可能会争辩说,这样做的应用程序已被破坏,但是     实际上,这是我们可以轻松地在文件系统级别上执行的操作     显着的性能问题,所以我们可以保持一致。 POSIX     提到这一点,XFS文件系统已经具备此功能。

  

这清楚地表明ext4 - 仅列举一个现代文件系统 - 在这方面不符合POSIX.1-2008。

答案 1 :(得分:8)

对标准强制要求的一些误解来自于流程与线程的使用,以及这对于您所谈论的“处理”情况意味着什么。特别是,你错过了这一部分:

  

可以通过显式用户操作创建或销毁句柄,而不会影响基础打开文件描述。 创建它们的一些方法包括fcntl(),dup(),fdopen(),fileno()和 fork() 。它们至少可以被fclose(),close()和exec函数破坏。 [...]请注意,在fork()之后,存在两个句柄,其中之前存在一个句柄。

来自上面引用的POSIX规范部分。 “创建[句柄使用] fork”的引用未在本节中进一步详细说明,但fork()的规范增加了一些细节:

  

子进程应具有其父级文件描述符的副本。每个子文件描述符都应引用相同的打开文件描述以及父文件的相应文件描述符。

这里的相关位是:

  • 孩子有副本父母的文件描述符
  • 孩子的副本指的是父母可以通过所述fds
  • 访问的“事物”
  • file descript ors 和文件 descript ion 同样的事情;特别是,文件描述符是上述意义上的句柄

这是第一个引用时所说的“fork()创建[...]句柄” - 它们被创建为副本,因此,从那时起,已分离,并且不再以锁步方式更新。

在您的示例程序中,每个子进程都会获得自己的副本,该副本从相同的状态开始,但在复制行为之后,这些文件描述符/句柄已成为独立实例,因此写作彼此竞争。关于标准,这是完全可以接受的,因为write()只是保证人:

  

在能够搜索的常规文件或其他文件上,实际的数据写入应从与fildes相关的文件偏移量所指示的文件中的位置开始。在从write()成功返回之前,文件偏移量应增加实际写入的字节数。

这意味着虽然它们都以相同的偏移量开始写入(因为fd copy 已初始化),但即使成功,它们也可能写入不同的数量(不能保证标准,N字节的写请求将完全 N个字节写入;它可以成功实现任何0 <=实际<= N),并且由于由于未指定写入的顺序,因此上面的整个示例程序具有未指定的结果。即使写入了总请求数量,上面的所有标准都表明文件偏移量递增 - 它没有说它是原子的(只有一次)递增,也没有说实际的数据写入将以原子方式发生。

但有一件事是可以保证的 - 你永远不应该在文件中看到任何写入之前没有任何内容,或者任何写入中没有写入任何数据的内容。如果你这样做,那就是腐败,以及文件系统实现中的一个错误。您在上面观察到的可能是......如果无法通过重新排序部分写入来解释最终结果。

O_APPEND的使用解决了这个问题,因为再次使用它 - 请参阅write(),确实:

  

如果设置了文件状态标志的O_APPEND标志,则应在每次写入之前将文件偏移设置为文件的末尾,并且在更改文件偏移和写入操作之间不应进行中间文件修改操作。 / p>

这是您寻求的“先于”/“无介入”序列化行为。

使用线程会部分改变行为 - 因为创建时线程不接收文件描述符/句柄的副本,而是在实际(共享)上运行一。线程不一定(必然)都以相同的偏移量开始写入。但是,部分写入成功的选项仍然意味着您可能会以您可能不希望看到的方式看到交错。但它可能仍然完全符合标准。

道德:不要指望POSIX / UNIX标准默认限制。在常见情况下,故意放宽规范,并要求您作为程序员明确表达您的意图。

答案 2 :(得分:8)

编辑: 2017年8月更新了操作系统行为的最新变化。

首先,Windows上的O_APPEND或等效的FILE_APPEND_DATA意味着并发编写器下最大文件范围(文件&#34;长度&#34;)的增量为原子。这是由POSIX保证的,Linux,FreeBSD,OS X和Windows都正确实现了它。 Samba也正确地实现了它,v5之前的NFS没有,因为它缺乏以原子方式追加的有线格式功能。因此,如果您使用仅附加文件打开文件,除非涉及NFS,否则并发写入不会在任何主要操作系统上相互撕裂

这没有说明读取是否会看到撕裂的写入,并且POSIX上说下面关于read()和write()的原子性如下:

  

以下所有函数都应该是原子函数   其他在POSIX.1-2008中指定的效果进行操作时   常规文件或符号链接... [很多功能] ... read()...   write()...如果两个线程分别调用其中一个函数,则每次调用   应该看到另一个呼叫的所有指定效果,或者   没有一个。 [Source]

  

写入可以针对其他读取和写入进行序列化。如果一个   文件数据的read()可以证明(通过任何方式)在a之后发生   write()的数据,它必须反映write(),即使调用   是由不同的过程。 [Source]

但反过来说:

  

此POSIX.1-2008卷未指定并发行为   从多个进程写入文件。应用程序应使用一些   并发控制的形式。 [Source]

对所有这三个要求的安全解释将表明,在同一文件中重叠某个范围的所有写入必须相互序列化,并且读取使得撕裂的写入永远不会出现给读者。

一个不太安全但仍然允许的解释可能是在同一进程内的线程之间只读取和写入彼此串行,并且进程之间的写入仅针对读取进行序列化(即,顺序一致的i / o排序在进程中的线程之间,但在进程之间i / o只是获取 - 释放)。

那么流行的操作系统和文件系统如何在此上执行?作为提出Boost.AFIO异步文件系统和文件i / o C ++库的作者,我决定编写一个经验测试人员。对于单个进程中的许多线程,结果如下。

否O_DIRECT / FILE_FLAG_NO_BUFFERING:

Microsoft Windows 10 with NTFS:update atomicity = 1 byte,包括10.0.10240,10.0.14393至少1Mb,根据POSIX规范,可能是无限的。

Linux 4.2.6 with ext4:update atomicity = 1 byte

带有ZFS的FreeBSD 10.2:update atomicity =至少1Mb,根据POSIX规范可能是无限的。

O_DIRECT / FILE_FLAG_NO_BUFFERING:

带有NTFS的Microsoft Windows 10:更新原子性=直到并包括10.0.10240仅在页面对齐时最多4096字节,否则如果FILE_FLAG_WRITE_THROUGH关闭则为512字节,否则为64字节。请注意,这个原子性可能是PCIe DMA的一个特性而不是设计的。自10.0.14393以来,至少1Mb,根据POSIX规范可能是无限的。

Linux 4.2.6 with ext4:update atomicity =至少1Mb,根据POSIX规范,可能是无限的。请注意,早期使用ext4的Linux肯定没有超过4096字节,XFS肯定用于自定义锁定,但看起来最近的Linux终于在ext4中解决了这个问题。

带有ZFS的FreeBSD 10.2:update atomicity =至少1Mb,根据POSIX规范可能是无限的。

总而言之,带有ZFS的FreeBSD和最近使用NTFS的Windows符合POSIX标准。最近使用ext4的Linux是POSIX,只符合O_DIRECT。

您可以在https://github.com/ned14/afio/tree/master/programs/fs-probe查看原始经验测试结果。注意我们只测试512字节倍数的撕裂偏移,所以我不能说在读 - 修改 - 写周期中是否会撕裂512字节扇区的部分更新。

答案 3 :(得分:6)

你误解了你引用的规范的第一部分:

  

文件描述符或流在其引用的打开文件描述中称为“句柄”;打开的文件描述可能有几个句柄。 [...]应用程序影响第一个句柄上文件偏移量的所有活动都应暂停,直到它再次成为活动文件句柄。 [...]句柄不需要在同一过程中应用这些规则。

这对处理并发访问的实现没有任何要求。相反,它要求应用程序不要进行并发访问,即使是来自不同的进程,如果你想要明确定义输出和副作用的顺序。

当写入大小适合PIPE_BUF时,唯一保证原子性的是管道。

顺便说一下,即使对write的调用对于普通文件来说是原子的,除了写入适合PIPE_BUF的管道的情况之外,write总是可以返回部分写入(即写入少于请求的字节数)。这个小于请求的写入将是原子的,但就整个操作的原子性而言,它根本不会有所帮助(您的应用程序必须重新调用write才能完成)。 p>