linux 进程之间的 write(2)/read(2) 原子性

write(2)/read(2) atomicity between processes in linux

我有一个案例,有两个进程作用于同一个文件——一个作为写入器,一个作为 reader。该文件是一个单行文本文件,编写者循环重写该行。 reader 读取该行。伪代码如下所示:

编写器进程

char buf[][18] = {
"xxxxxxxxxxxxxxxx",
"yyyyyyyyyyyyyyyy"
};
i = 0;
while (1) {
 pwrite(fd, buf[i], 18, 0);
 i = (i + 1) % 2;
}

Reader 进程

while(1) {
  pread(fd, readbuf, 18, 0);
  //check if readbuf is either buf[0] or buf[1]
}

经过 运行 两个过程一段时间后,我可以看到 readbufxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyxx

我的理解是写入对于最大 512 字节的大小是原子的。但从我的实验来看,原子性似乎只适用于 16 个字节。

手册页没有说明普通文件的原子性,它只提到了 512 字节的管道原子性。

我已经用 tmpfs 和 ext4 试过了,结果是一样的。使用 O_SYNC,ext4 写入成为原子,我理解它,因为写入不会 return 直到它到达磁盘,但 O_SYNC 对 tmpfs 没有帮助(/dev/shm) .

POSIX 不为 read and write 提供任何原子操作的最低保证,除了在管道上写入(其中最多 PIPE_BUF (≥ 512) 字节的写入是保证是原子的,但读取没有原子性保证)。 readwrite的操作是用字节值来描述的;除了管道之外,与围绕单字节 write 操作的循环相比,write 操作不提供额外的保证。

我不知道 Linux 会提供任何额外的保证,无论是 16 还是 512。实际上我希望它取决于内核版本、文件系统,可能还取决于其他底层块设备、CPU数量、CPU架构等因素

O_SYNCO_RSYNCO_DSYNC 保证(POSIX 的 synchronized I/O data integrity completion, given for read and write in the optional SIO 特性)不是您所需要的。它们保证在 readwrite 系统调用之前将写入提交到持久存储,但不对在 read 操作期间启动的 write 做出任何声明进行中。

在您的场景中,读取和写入文件看起来不是正确的工具集。

  • 如果您只需要传输少量数据,请使用管道。不要太担心复制:在大多数处理或上下文切换的规模上,复制内存中的数据非常快。再加上Linux在优化副本方面相当不错。
  • 如果您需要传输大量数据,您可能应该使用某种形式的内存映射:如果不需要磁盘支持,则使用共享内存段,或者 mmap 如果需要。这并不能神奇地解决原子性问题,但可能会提高适当同步机制的性能。要执行同步,有两种基本方法:
    • 生产者将数据写入共享内存,然后向消费者发送一个通知,指明哪些数据可用。消费者仅根据请求处理数据。通知可以使用相同的通道(例如 mmap + msync)或不同的通道(例如管道)。
    • 生产者将数据写入共享内存,然后刷新写入(例如msync)。然后,生产者将一个众所周知的值写入一个机器字(sig_atomic_t 通常会起作用,即使它的原子性在形式上只对信号有保证——或者,在实践中,uintptr_t)。消费者读取那个机器字,只有当这个字有可接受的值时才处理相应的数据。

PIPE_BUF 原子性要求适用于管道和 FIFO。 POSIX 给出 常规文件有不同的原子性要求,但 Linux 内核没有 符合。 regular-file 原子性要求出现在 2.9.7 Thread 与常规文件的交互 操作。 每当 write() 实现 return 某个正值 N 时,整个 N-byte 写应该是原子的。 (符合标准的 write() 实现可以选择 总是 return 一个小于或等于 1 的值,每次只接受一个字节 时间,在这种情况下原子性没有实际好处。)

虽然有些人 publicly argued regular-file 原子性仅适用于共享进程的线程,有 POSIX 写“two threads”没有先例,意思是“two threads of the 相同的过程”。此外,关于“的部分也适用于文件 描述符已成功关闭,但是导致(例如[...]进程 终止)”在与一个线程隔离的要求中是多余的 过程。