rename()是原子性的吗?

53

我无法通过实验来检查这个问题,也无法从手册中收集到相关信息。

假设我有两个进程,一个将文件1从目录1移动(rename)到目录2。同时另一个进程正在复制目录1和目录2的内容到另一个位置。是否可能出现这样的情况,即在第一个进程移动之前复制了目录1,并且在移动后复制了目录2,导致目录1和目录2都显示文件1。

基本上,rename()是原子系统调用吗?

谢谢。

4个回答

33

是和否。

假设操作系统没有崩溃,rename()是原子性的。它不能被任何其他文件系统操作拆分。

如果系统崩溃,您可能会看到ln()操作。

还要注意,在网络文件系统上操作时,当操作成功时,您可能会收到ENOENT错误。本地文件系统不会这样做。


阅读在线手册第2节中的man页面,包括link()、rename()、creat()、open()、unlink()、read()和write()。有两个API用于显式锁定,但忽略它们的程序可以轻松通过锁定。此外,在网络文件系统上不要信任flock()和相关函数(无参考:您应该知道这是计算机科学中无法解决的问题,实现可能会失败)。O_EXCL并不存在,这在过去使事情更加困难。 - Joshua
10
我的来源是“如果系统崩溃,您可能会看到一个ln()操作。”就是内核源代码本身。 - Joshua
1
ln() 操作用于创建硬链接。一旦在新位置创建完成,就会在旧位置执行 remove() 操作。这就是重命名操作。虽然这两个操作都是原子性的。 - Alexis Wilke
1
内核实现取决于文件系统,但这里是Linux ext4文件系统的实现:https://elixir.bootlin.com/linux/v5.19.3/source/fs/ext4/namei.c#L3887 - 似乎首先将inode标记为脏,然后创建新链接,之后删除旧链接。如果内核在中途崩溃,则最终结果可能是两个链接和脏标记。我猜测(但没有调查是否正确),下一次挂载期间的日志恢复将修复最终结果,以匹配留下旧状态或新状态的原子行为,具体取决于精确的崩溃位置。 - Mikko Rantalainen
1
@MikkoRantalainen:很可能,你是正确的。不同的文件系统做不同的事情。minix fs(因为它非常简单而方便学习)肯定有一种故障模式,你会看到一个ln操作(带有一个错误的链接计数,将由fsck修复)。 - Joshua
显示剩余2条评论

25
这个回答可能有点晚,但是......是的,rename() 是原子的,但不是你问题中的那种。 在Linux下,rename(2)说:

然而,在覆盖时,oldpath和newpath都指向正在重命名的文件的时间窗口可能会出现。

但是rename()在非常重要的意义上仍然是原子的:如果您使用它来覆盖文件,则最终将得到旧版本或新版本之一,没有其他东西。

[更新:但正如@jonas-wielicki在评论中指出的那样,您需要确保正在重新命名的文件实际上具有最新的内容,使用fsync()等工具进行同步]

如果newpath已经存在,它将被原子替换(受几个条件的限制;请参见下面的错误),因此没有另一个进程尝试访问newpath会发现它丢失的时刻。

如果看到 ERRORS,您会发现重命名可能会失败,但永远不会破坏原子性。
这全部来自Linux手册页。 我不知道的是,如果您在运行不同操作系统的服务器上运行网络文件系统,是否可以保证客户端确保原子性。 我怀疑。

2
重命名文件夹是否也适用于同样的情况? - proteneer
1
@proteneer 是的,但可能不是你想要的方式。除非它是空的,否则您不允许覆盖现有目录。对于空目录,我猜您会获得原子性保证。 - Adrian Ratnapala
4
在覆盖的情况下,重命名之前调用flush非常重要,以确保数据实际上被写入文件。否则,在崩溃的情况下,可能最终只得到一个空文件(rename()成功,但数据尚未写入磁盘,然后崩溃->空文件)。 - Jonas Schäfer

7
我不确定你问题中的“基本上”部分是否有效。除非两者之间存在某种同步,否则原子重命名的方式并不重要。如果目录复制在重命名之前到达,则文件1将出现在两个位置。
我不确定你是指线程还是进程,但如果两者都有锁定机制,则线程锁定明显更简单,因为它们无需跨进程边界。

如果正确使用,rename() 是在工作进程之间分配工作负载的一种完全合法的方式。 - Joshua
3
@Joshua:是的,但Mark0978说得对:尽管在一个文件系统上的“重命名(rename())”是原子性的(因为读取两个不同目录的内容不是原子操作),但OP所描述的过程是有竞争条件的,这意味着在你读取directory1和directory2之间进行重命名操作。请注意,虽然“重命名(rename())”是原子性的,但该过程仍然存在竞争条件。 - caf
@caf:Mark正在使用草人论点。你需要从两个进程中都使用rename()函数。 - Joshua
1
这不是一个草人论点。他想要重命名是原子性的,暗示着他不希望在重命名时被中断。他暗示要使用多个进程或线程(不确定他知道两者之间的区别),并希望确保重命名不会在复制过程中被卡住。所有这些都指向需要更好地理解竞态条件的人。他在这里问错了问题,担心了错误的事情。当汽车驶向峡谷燃烧时,汽车的颜色并不重要。 - boatcoder
3
@Joshua: 你需要重新阅读原始问题。它有一个进程在执行rename(),同时还有另一个进程在将"directory1和directory2的内容复制到另一个位置"。第二个进程至少需要进行不同的“读取directory1的内容”和“读取directory2的内容”的步骤,甚至在开始任何复制/重命名操作之前,正是这些步骤会与第一个进程中的rename()竞争。 - caf
1
除非Juggler在第二个进程中并不是想说“复制”,否则我必须同意rename()的原子性与所提到的问题无关。 - Alexis Wilke

0

GNU libc 手册中提到:

rename的一个有用特性是,newname的含义会“原子地”从之前任何已存在的同名文件更改为其新含义(即被称为oldname的文件)。在旧含义和新含义之间不存在newname不存在的瞬间。如果操作期间系统崩溃,则可能同时存在两个名称;但只要newname存在,它就始终完好无损。


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