2010-07-11 96 views
6

如果我要重命名AB,但只有当B不存在,天真的事情会被检查是否存在B(与access("B", F_OK)或类似的东西),如果它不与rename进行。不幸的是,这会打开一个窗口,在此窗口期间,其他进程可能会决定创建B,然后它会被覆盖 - 更糟糕的是,没有迹象表明发生过类似情况。如何在没有竞争条件的情况下重命名()?

其他文件系统访问功能不从这个痛苦 - openO_EXCL(所以复制的文件是安全的),而最近的Linux得到了*at系统调用是防止大多数其他竞争条件的整个家庭 - 但不是这个特殊的(renameat存在,但防止完全不同的问题)。

那么它有一个解决方案?

+0

也许您应该考虑使用显式锁定机制,而不是依赖隐式锁定(包装)重命名函数。如果创建'B'是您控制的程序,则可以使用进程间同步原语。 – 2010-07-11 09:37:55

回答

14

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

+0

什么是在不同的挂载点下的文件? – 2010-07-11 17:47:11

+2

@Moron:rename()不适用于不同的文件系统。 – Dummy00001 2010-07-11 19:54:51

+0

@ Dummy00001:我明白了。我错过了标题中的括号。 +1然后:-) – 2010-07-11 21:11:39

1

从命名手册页:

如果NEWPATH已经存在,它将被 原子代替(但有几个 条件;见下文错误),使 没有一点在其另一 尝试访问新路径 的进程会发现它丢失。

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

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

+0

如果文件B不存在,如果您测试是否存在,则仍然存在竞争条件 - 两个进程都可能检测到它不存在,然后两者都进行重命名,并关于最后哪个进程重命名的结果具有不确定的结果。唯一的解决方案是rename()是原子的,如果新的文件名已经存在,就会失败,这就是它在Windows上所做的,以及我认为C标准指定的(看起来不正确)。 – 2010-07-11 08:58:33

6

你可以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),随机)
  3. MKDIR(T),如果失败循环到以前的步骤
  4. 开放(C(T, “锁”),O_EXCL ),如果失败命令rmdir(T)忽略错误和循环到以前的步骤
  5. 重命名(S,C(T, “TMP”))
  6. 链路(C(T, “TMP”),d)
  7. 的unlink(C(T, “TMP”))
  8. 的unlink(C(T, “锁定”))
  9. 命令rmdir(T)

算法safe_rename(S,D)解释说:

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

要正确做到rename(),我们需要一些地方隐藏起来。所以我们构建一个目录的方式可以确保没有其他人(谁遵循相同的算法)意外地使用它。

但是mkdir()不能保证在NFS上原子化。因此,我们需要确保我们有一定的保证,我们在目录中独处。这是lockfile上的O_EXCL。严格来说,这不是锁定,而是信号量。

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

既然我们有一个大部分都是空的目录,那么我们就可以安全地在rename()的源代码那里。这确保没有其他人改变来源,直到我们将unlink()它。 (好吧,内容可以改变,这不是问题。)

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

然后unlink()可以在剩余的来源完成比赛条件免费。剩下的就是清理。

只有一个问题留给:

万一link()失败,我们已经搬到源。为了正确清理,我们需要将其移回。这可以通过调用safe_rename(C(T,"tmp"),S)来完成。如果这也失败了,我们所能做的就是尝试尽可能多地清理(unlink(C(T,"lock")),​​),并让管理员手动清理垃圾。

最后说明:

为了帮助碎片情况进行清理,你都不可能使用一些更好的文件名比tmp。聪明地选择名字也可以使算法免受攻击。

如果你正在移动文件的列车载入文件,你可以重复使用目录。

但是,我同意,这个算法是纯粹的矫枉过正和O_EXCL之类的rename()丢失。