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

显然POSIX说明了这一点

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

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

我对此的理解是,当第一个进程发出write(handle, data1, size1)而第二个进程发出write(handle, data2, size2) ,写入可以按任何顺序发生,但data1data2 必须都是原始的和连续的。

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

 #include  #include  #include  #include  #include  #include  #include  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)。

open(2)调用中添加标志O_APPEND open(2)解决此问题。 很好,但我不明白为什么。 POSIX说

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

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

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

从两个进程对文件进行primefaces写入并不专门解决进程共享相同文件描述 (与同一文件相对)的情况。

如何以编程方式确定“写入”系统调用对特定文件是否是primefaces的? 说

POSIX中定义的write调用根本没有primefaces性保证。

但如上所述它确实有一些。 而且, O_APPEND似乎触发了这种primefaces性保证,尽管在我看来,即使没有O_APPEND ,这种保证也应该存在。

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

man 2 write在我的系统上总结得很好:

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

以下是对ext4邮件列表最近讨论的引用:

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

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

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

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

从上面引用的POSIX规范部分。 “create [handles using] fork ”的引用在本节中没有详细说明,但fork()的规范增加了一些细节:

子进程应拥有自己父文件描述符的副本 。 每个子文件描述符都应引用与父对应文件描述符相同的打开文件描述。

这里的相关位是:

  • 子项具有父项文件描述符的副本
  • 孩子的副本指的是父母可以通过所述fds访问的“事物”
  • 文件描述 ors和文件描述 离子 不是一回事; 特别地,文件描述符是上述意义上的句柄

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

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

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

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

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

使用O_APPEND修复了这个问题,因为再次使用它 - 请参阅write() ,它会:

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

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

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

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

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

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

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

当它们在常规文件或符号链接上运行时,所有以下函数在POSIX.1-2008中指定的效果中相互之间应该是primefaces的… [许多函数] … read()… write( )…如果两个线程分别调用其中一个函数,则每个调用应该看到另一个调用的所有指定效果,或者没有一个。 [资源]

写入可以相对于其他读取和写入进行序列化。 如果在数据的write()之后可以certificate(通过任何方式)文件数据的read(),它必须反映write(),即使调用是由不同的进程完成的。 [资源]

但反过来说:

此卷POSIX.1-2008未指定从多个进程并发写入文件的行为。 应用程序应该使用某种forms的并发控制。 [资源]

对所有这三个要求的安全解释将表明,在同一文件中重叠一定范围的所有写入必须相对于彼此进行序列化,并且读取使得撕裂的写入从未出现在读者身上。

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

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


否O_DIRECT / FILE_FLAG_NO_BUFFERING:

带有NTFS的Microsoft Windows 10:更新primefaces性= 1个字节,直到并包括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:仅当页面对齐时,更新primefaces性=直到并包括10.0.10240最多4096字节,否则如果FILE_FLAG_WRITE_THROUGH关闭则为512字节,否则为64字节。 请注意,这个primefaces性可能是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字节扇区的部分更新。

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

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

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

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

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