【问题标题】:How to rename() without race conditions?如何在没有竞争条件的情况下重命名()?
【发布时间】:2010-07-11 08:14:46
【问题描述】:

如果我想将A 重命名为B,但前提是B 不存在,那么天真的事情就是检查B 是否存在(使用access("B", F_OK) 或类似的东西),并且如果它没有继续rename。不幸的是,这会打开一个窗口,在此期间其他进程可能会决定创建 B,然后它会被覆盖 - 更糟糕的是,没有迹象表明曾经发生过类似的事情。

其他文件系统访问函数不会受此影响 - openO_EXCL(因此复制文件是安全的),最近 Linux 获得了整个系列的 *at 系统调用,可以防止大多数其他竞争条件 -但不是这个特定的(renameat 存在,但可以防止完全不同的问题)。

那么它有解决方案吗?

【问题讨论】:

  • 也许您应该考虑使用显式锁定机制,而不是依赖于重命名函数(的包装器)中的隐式锁定。如果创建B 的是您控制的程序,您可以使用进程间同步原语。

标签: linux unix race-condition


【解决方案1】:

你应该可以link(2) 到新的文件名。如果链接失败,那么您放弃,因为该文件已经存在。如果链接成功,您的文件现在以旧名称和新名称存在。然后你unlink(2) 旧名字。没有可能的竞争条件。

【讨论】:

  • @Dummy00001:我明白了。我错过了标题中的括号。然后 +1 :-)
  • 但是这只是将可能的竞争条件转移到unlink() 一侧。
  • 不,unlink() 仅在您获得链接时才会发生,并且保证是原子的。
  • 如果您在 NFS 等慢速文件系统上的重负载机器上同时在同一源上运行多个不同进程,这种原始算法可能会造成严重危害,包括 @987654324 @ 按以下顺序新创建的新源,例如:a:linkb:linka:unlinkc:creatc:write b:unlink -> 由 c 重新创建的文件消失。
  • 不是这样。如果 a:link 成功,则 b:link 将失败 - 无论负载和多个进程如何。如果 b:link 失败,你永远不会执行 b:unlink。
【解决方案2】:

您可以将现有文件link() 使用所需的新文件名,然后删除现有文件名。

link() 只有在新路径名不存在时才能成功创建新链接。

类似:

int result = link( "A", "B");

if (result != 0) {
    // the link wasn't created for some reason (maybe because "B" already existed)
    // handle the failure however appropriate...
    return -1;
}

// at this point there are 2 filenames hardlinked to the contents of "A", 
//   filename "A" and filename "B"

// remove filename "A"
unlink( "A");

link() 的文档中讨论了此技术(请参阅有关修改 passwd 文件的讨论):

【讨论】:

    【解决方案3】:

    很抱歉在旧线程中添加了一些内容。并且写了这么长的帖子。

    我只知道一种方法可以在没有锁定的情况下实现完全免费的竞争条件rename(),这几乎可以在任何文件系统上工作,即使是在服务器间歇性重启和客户端时间扭曲到位的 NFS 上也是如此。

    以下配方是无竞争条件的,因为在任何情况下数据都不会丢失。它也不需要锁,并且可以由不想合作的客户端执行,除非它们都使用相同的算法。

    从某种意义上说,如果某些东西严重损坏,一切都会保持干净整洁的状态,这不是无竞争条件。它也有一段很短的时间,源头和目的地都不在他们的位置,但是源头仍然以另一个名字存在。并且对于攻击者试图造成伤害的情况(rename() 是罪魁祸首,去看看),它并没有得到强化。

    S是源,D是目的地,P(x)是dirname(x),C(x,y)是x/y路径拼接

    1. 检查目标不存在。只是为了确保我们不会徒劳地执行后续步骤。
    2. 创建一个可能唯一的名称 T := C(P(D),random)
    3. mkdir(T),如果失败则循环到上一步
    4. open(C(T,"lock"),O_EXCL),如果失败 rmdir(T) 忽略错误并循环到上一步
    5. 重命名(S,C(T,"tmp"))
    6. 链接(C(T,"tmp"),D)
    7. 取消链接(C(T,"tmp"))
    8. 取消链接(C(T,"lock"))
    9. rmdir(T)

    算法safe_rename(S,D)解释:

    问题是我们要确保不存在竞争条件,无论是在源上还是在目的地上。假设在每个步骤之间(几乎)任何事情都可能发生,但所有其他进程在进行无竞争条件重命名时都遵循完全相同的算法。这包括临时目录 T 永远不会被触及,除非在确保(这是一个手动过程)使用该目录的进程已经死亡并且无法复活(例如在恢复后继续 VM 休眠)之后。

    要正确执行rename(),我们需要一个地方躲起来。因此,我们构建了一个目录,以确保没有其他人(遵循相同算法的人)会意外使用它。

    但是,mkdir() 不能保证在 NFS 上是原子的。因此,我们需要确保我们有一些保证,我们在目录中是单独的。这是锁定文件上的O_EXCL。这 - 严格来说 - 不是锁定,它是一个信号量。

    除了这种罕见的情况,mkdir() 通常是原子的。我们还可以为目录创建使用一些加密安全的随机名称,添加一些 GUID、主机名和 PID,以确保其他人不太可能偶然选择相同的名称。然而,为了证明算法是正确的,我们需要这个名为 lock 的文件。

    现在我们有一个大部分为空的目录,我们可以安全地rename() 那里的源。这样可以确保在我们unlink() 之前没有其他人更改源。 (嗯,内容可以改变,这不是问题。)

    现在可以应用link() 技巧来确保我们不会覆盖目标。

    之后,unlink() 可以在剩余的源上免费完成竞争条件。剩下的就是清理了。

    只剩下一个问题了:

    如果link() 失败,我们已经移动了源。为了进行适当的清理,我们需要将其移回。这可以通过调用safe_rename(C(T,"tmp"),S) 来完成。如果这也失败了,我们所能做的就是尽可能多地清理(unlink(C(T,"lock"))rmdir(T))并留下碎片以供管理员手动清理。

    最后的笔记:

    为了帮助清理碎片情况,您可以使用比tmp 更好的文件名。巧妙地选择名称也可以在一定程度上强化算法以抵御攻击。

    如果您将大量文件移动到某个地方,您当然可以重用该目录。

    但是,我同意,这种算法显然是矫枉过正,并且缺少像 rename() 上的 O_EXCL 这样的东西。

    【讨论】:

      【解决方案4】:

      从 Linux 内核 3.15(2014 年 6 月发布)开始,这可以通过 syscall(__NR_renameat2, AT_FDCWD, "source-file", AT_FDCWD, "dest-file", RENAME_NOREPLACE) 来完成(包括 <syscall.h>, @987654323 @ 和 <linux/fs.h>)。

      这比 link() 好,因为永远不会有两个文件名同时存在的点(特别是使用 link(),精确定时的断电可能会导致两个文件名永久保留)。

      glibc 2.28(2018 年 8 月发布)添加了 renameat2() 包装器,因此您可以使用它来代替 syscall.h 和 linux/fs.h(尽管您很可能需要 <stdio.h>#define __GNU_SOURCE )。

      有关更多详细信息,请参阅 http://man7.org/linux/man-pages/man2/rename.2.html(尽管在撰写本文时它确实不知道 glibc 现在有一个 renameat2 包装器)。

      【讨论】:

      • > 与链接(),精确定时的停电可能导致两个名称永远保留重命名也是如此; atomic 仅适用于正常运行的内核。断电的行为取决于正在使用的文件系统(除其他外)。重命名仍然比链接好,因为终止进程可能会泄漏两个文件,就像示例中一样
      【解决方案5】:

      来自重命名手册页:

      如果 newpath 已经存在,它将是 原子替换(受少数 条件;见下面的错误),这样 没有一点可以让另一个 试图访问新路径的进程 会发现它不见了。

      因此,当B 文件已存在时,无法避免重命名。我想你可能别无选择,只能在尝试重命名之前检查是否存在(使用stat() 而不是access()),如果你不希望在文件已经存在的情况下发生重命名。忽略竞争条件。

      否则,下面提供的带有 link() 的解决方案似乎符合您的要求。

      【讨论】:

      • 如果文件 B 不存在,如果您测试是否存在,仍然存在竞争条件 - 两个进程都可能检测到它不存在,然后都进行重命名,结果不确定最后重命名的过程。唯一的解决方案是 rename() 是原子的,并且如果新文件名已经存在则失败,这就是它在 Windows 上所做的,以及我认为(看起来不正确)指定的 C 标准。
      猜你喜欢
      • 2013-12-23
      • 2023-02-09
      • 2013-07-16
      • 1970-01-01
      • 2012-03-31
      • 2016-05-17
      • 2021-02-14
      • 2020-11-08
      • 1970-01-01
      相关资源
      最近更新 更多