18

从这里开始:文件附加在 UNIX 中是原子的吗

考虑多个进程打开同一个文件并附加到它的情况。O_APPEND 保证寻找到文件末尾然后开始写操作是原子的。因此,多个进程可以附加到同一个文件,只要每个写入大小 <= PIPE_BUF,任何进程都不会覆盖任何其他进程的写入。

我编写了一个测试程序,其中多个进程打开并写入同一个文件(write(2))。我确保每个写入大小 > PIPE_BUF (4k)。我期待看到进程覆盖其他人的数据的情况。但这不会发生。我测试了不同的写入大小。这只是运气还是没有发生这种情况的原因?我的最终目标是了解附加到同一文件的多个进程是否需要协调它们的写入。

这是完整的程序。每个进程创建一个 int 缓冲区,用它的 填充所有值rank,打开一个文件并写入它。

规格:OpenMPI 1.4.3 on Opensuse 11.3 64-bit

编译为:mpicc -O3 test.c,运行为:mpirun -np 8 ./a.out

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int 
main(int argc, char** argv) {
    int rank, size, i, bufsize = 134217728, fd, status = 0, bytes_written, tmp_bytes_written;
    int* buf;
    char* filename = "/tmp/testfile.out";

    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);

    buf = (int*) malloc (bufsize * sizeof(int));   
    if(buf == NULL) {
        status = -1;
        perror("Could not malloc");
        goto finalize;
    }
    for(i=0; i<bufsize; i++) 
        buf[i] = rank;

    if(-1 == (fd = open(filename, O_APPEND|O_WRONLY, S_IWUSR))) {
        perror("Cant open file");
        status = -1;
        goto end;
        exit(-1);
    }

    bytes_written = 0;
    if(bufsize != (tmp_bytes_written = write(fd, buf, bufsize))) {
        perror("Error during write");
        printf("ret value: %d\n", tmp_bytes_written);
        status = -1;
        goto close;
    }

close:
    if(-1 == close(fd)) {
        perror("Error during close");
        status = -1;
    }
end:
    free(buf);
finalize:
    MPI_Finalize();
    return status;
}
4

3 回答 3

18

小于的写入原子性PIPE_BUF仅适用于管道和 FIFO。对于文件写入,POSIX 说:

本卷 POSIX.1-2008 未指定从多个进程并发写入文件的行为。应用程序应该使用某种形式的并发控制。

...这意味着您只能靠自己 - 不同的 UNIX-like 将提供不同的保证。

于 2012-10-17T21:10:27.830 回答
16

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

这并没有说明读取是否会看到撕裂的写入,并且在那个 POSIX 上说以下关于 read() 和 write() 对常规文件的原子性:

以下所有函数在对常规文件或符号链接进行操作时,在 POSIX.1-2008 中指定的效果中相互之间应是原子的... [许多函数] ... read() ... write( ) ... 如果两个线程各自调用这些函数之一,则每个调用要么看到另一个调用的所有指定效果,要么一个都看不到。[资源]

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

但反过来:

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

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

一个不太安全但仍然允许的解释可能是读取和写入仅在同一进程内的线程之间相互序列化,而进程之间的写入仅相对于读取进行序列化(即在线程之间存在顺序一致的 i/o 顺序)一个进程,但进程之间的 i/o 只是获取-释放)。

当然,仅仅因为标准需要这些语义并不意味着实现符合要求,尽管事实上带有 ZFS 的 FreeBSD 表现完美,最近带有 NTFS 的 Windows (10.0.14393) 表现完美,如果 O_DIRECT 开启,最近带有 ext4 的 Linux 表现正确. 如果您想了解有关主要操作系统和文件系统是否符合标准的更多详细信息,请参阅此答案

于 2016-02-07T22:45:35.837 回答
6

这不是运气,从某种意义上说,如果您深入研究内核,您可能可以证明在您的特定情况下,永远不会发生一个进程write与另一个进程交错的情况。我假设:

  • 您没有达到任何文件大小限制
  • 您没有填充创建测试文件的文件系统
  • 该文件是一个常规文件(不是套接字、管道或其他东西)
  • 文件系统是本地的
  • 缓冲区不跨越多个虚拟内存映射(这个是众所周知的,因为它是malloc()ed,它把它放在堆上,它是连续的。
  • write()进程在忙碌时不会被中断、发出信号或跟踪。
  • 没有磁盘 I/O 错误、RAM 故障或任何其他异常情况。
  • (也许是其他人)

您可能确实会发现,如果所有这些假设都成立,那么您碰巧使用的操作系统的内核总是通过write()对以下文件的单个原子连续写入来完成单个系统调用。

这并不意味着你可以指望这总是正确的。你永远不知道什么时候它可能不是真的:

  • 该程序在不同的操作系统上运行
  • 文件移动到 NFS 文件系统
  • 进程在进程中收到信号write()write()返回部分结果(比请求的字节少)。不确定 POSIX 是否真的允许这种情况发生,但我进行了防御性编程!
  • ETC...

因此,您的实验无法证明您可以依靠非交错写入。

于 2012-10-17T21:11:10.480 回答