在Unix中,写操作是原子操作吗?

6
我正在阅读《UNIX环境高级编程》(APUE),当我看到 $3.11 时,遇到了这个问题:
if (lseek(fd, 0L, 2) < 0) /* position to EOF */
err_sys("lseek error");
if (write(fd, buf, 100) != 100) /* and write */
err_sys("write error")

APUE的书中提到:

这种方法对于单个进程来说可以正常工作,但是如果多个进程使用这种技术来附加到同一个文件,则会出现问题。......问题在于我们的逻辑操作“定位到文件结尾并写入”需要两个单独的函数调用(如我们所示)。 任何需要超过一个函数调用的操作都不可能是原子操作,因为内核始终有可能在两个函数调用之间暂时挂起进程。

这段话只是说在lseekwrite之间的函数调用会被CPU打断,我想知道在write的一半操作中是否也会被打断?或者说,write是原子的吗?如果线程A写入"aaaaa",线程B写入"bbbbb",结果会是"aabbbbbaaa"吗?

此外,APUE还提到preadpwrite都是原子操作,这是否意味着这些函数在内部使用了mutexlock来实现原子性?


写入是原子性的。没有例外。如果两个进程同时写入(同一个文件),那么两个写入都是原子性的。如果它们恰好重叠:第二个写入将覆盖第一个写入的一部分。(反之亦然)处理这种重叠情况是用户进程的责任。内核只保证原子性的单个写入。 - joop
2
我将此关闭为重复问题,因为链接的问题中有很好的答案。 - rici
@joop 虽然 write() 可能是原子的,但不能保证它是完整的。根据POSIX write()文档:“write()函数将尝试写入nbyte字节…”虽然“短”写入在我写入本地文件时确实是我从未见过的。 - Andrew Henle
1
@andrewHenle:短写入并不是失败,而且在写入尝试扩展文件超出文件系统容量的情况下是必需的:“如果write()请求写入的字节数超过了可用空间(例如,进程的文件大小限制或介质的物理结尾),则只会写入尽可能多的字节。”仅当无法写入任何字节时才算失败。 - rici
1
@andrew:抱歉,我想我误解了你评论的重点。我不会说写入是原子性的(除了为管道和FIFO提供小写入的情况),而短写入的可能性就是其中之一。成功的写入是按某种方式排序的,而由于短写入是成功的,它们也是按某种顺序执行的。但这远非原子性的声明。 - rici
显示剩余6条评论
2个回答

9
调用Posix语义“原子”可能是一种过度简化。Posix要求读写以某种顺序发生:
写操作可以与其他读写操作串行化。如果可以通过任何方式证明文件数据的读取发生在数据的写入之后,它必须反映该写入操作,即使调用是由不同进程进行的。类似的要求也适用于对同一文件位置的多个写操作。这是为了保证数据从写操作传播到随后的读操作。 (来自 Posix规范中关于pwritewrite的解释的“基本原理”部分)
APUE提到的原子性保证是指使用O_APPEND标志,它强制将写操作执行到文件末尾。
如果文件状态标志的O_APPEND标记被设置,则在每次写入之前,文件偏移量将设置为文件末尾,并且在更改文件偏移量和写操作之间不会发生任何干扰的文件修改操作。
关于preadpwrite,APUE(Advanced Programming in the UNIX Environment)指出(当然是正确的),这些接口允许应用程序搜索并以原子方式执行I/O;换句话说,无论其他进程做什么,I/O操作都将发生在指定的文件位置。(因为该位置在调用本身中指定,不影响持久文件位置。)
Posix排序保证如下(来自write()pwrite()函数的描述):
在对常规文件的write()成功返回后,对于该write()修改的每个字节位置的任何成功的read()都应返回该位置的write()指定的数据,直到再次修改这些字节位置。对于同一文件中相同字节位置的任何后续成功的write()都将覆盖该文件数据。如Rationale中所述,这种措辞确保两个同时进行的write()调用(即使在不同的无关进程中)也不会交错数据,因为如果在最终成功的第二个保证期间交错了数据,则无法提供第二个保证。如何实现由实现决定。必须注意,并非所有的文件系统都符合Posix,而模块化的操作系统设计允许多个文件系统共存于单个安装中,这使得内核本身无法为适用于所有可用文件系统的write()提供保证。网络文件系统特别容易出现数据竞争(本地互斥锁也不会有太大帮助),这也是Posix所提到的(从Rationale引用的段落的末尾)。
这个要求对于网络文件系统尤为重要,因为某些缓存方案违反了这些语义。
第一个保证(关于后续读取)需要在文件系统中进行一些簿记工作,因为已经成功“写入”内核缓冲区但尚未同步到磁盘的数据必须对从该文件读取的进程透明可用。这还需要一些内部锁定内核元数据。
由于通常通过内核缓冲区来写入普通文件,并且实际上将数据同步到物理存储设备绝对不是原子操作,因此提供这些保证所需的锁定时间不必非常长。但是,它们必须在文件系统内完成,因为 POSIX 的措辞没有限制这些保证仅限于单线程进程中的同时写入。
在多线程进程中,Posix 要求 read()write()pread()pwrite() 在操作普通文件(或符号链接)时是原子的。请参阅 Thread Interactions with Regular File Operations 以获取必须遵守此要求的接口的完整列表。

如果两个线程都调用 [write() 函数],则每个调用应该能够看到另一个调用的所有指定效果,或者完全看不到。 - choxsword
@bigxiao:在单个进程中,write 是原子的。我在答案的最后一段链接了 Posix 的相关部分。但是在我看来,这并不能真正使调用变为原子操作,因为 write 作用于一个全局对象,可见性超出了单个进程的上下文范围。在进程之间,保证会更弱,尽管仍然有某种保证。 - rici
@bigxiao:我承认这是语义学。在我的第一段中,我没有说“write不是原子性的”。我说的是称其为“原子性”的说法过于简单化了。有很多小的边角情况。 - rici
在单进程的多线程中,它保证是原子性的,但在多进程中不是吗? - choxsword
@bigxiao:在进程之间,POSIX 规定如果你能证明一个读操作发生在一个写操作之后,那么这个读操作将会看到该写操作的结果。这是一个重要的保证,但我认为它并不像“原子性”这个词所表达的那样强。 (此外,实际上这并不是真的。文件系统/主机可能在更新提交到物理存储之前崩溃;在重新启动后,write() 可能不再可见。我并不是说这有什么问题;只是它可能不符合每个人对“原子性”的理解。) - rici

-3
在Linux中,有阻塞和非阻塞系统调用。 write 是阻塞系统调用的一个例子,这意味着执行线程将被阻塞,直到 write 完成。因此,一旦用户进程调用了 write,它就不能执行其他任何操作,直到系统调用完成。因此,从用户线程的角度来看,它会表现得像原子操作[尽管在内核级别上可能会发生很多事情,并且内核对系统调用的执行可能会被多次中断]。

如果CPU有多个核心,不同的线程在不同的核心上运行(硬件并发),那么所有核心都会被用户级别阻塞吗? - choxsword
一个线程在任何时刻只能在一个核心上执行。同一个系统调用可以由不同的线程从许多核心执行,并且这些线程中的每一个都被阻塞。内核有锁来确保相同系统调用的多个实例不会破坏公共资源。 - Ketan Mukadam
那么结果在任何情况下都不会是“aabbbbbaaa”吗? - choxsword
我的问题是假设多个线程正在访问同一个文件。这也是APUE所讨论的内容。 - choxsword
让我们在聊天中继续这个讨论 - choxsword
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接