移动目录的原子操作

48

我有两个目录在同一个父目录下。称父目录为base,子目录为alphabravo。我想要用bravo替换alpha。最简单的方法是:

rm -rf alpha
mv bravo alpha

mv命令是原子性的,但rm -rf命令不是。在bash中有没有一种简单的方法来原子性地将alpha替换为bravo?如果没有,是否有一种复杂的方法?

补充:

顺便提一句,如果目录在短时间内不存在,这并不是无法解决的问题。只有一个地方尝试访问alpha,并且在执行任何关键操作之前会检查alpha是否存在。如果不存在,则会给出错误消息。但如果有一种方法可以做到这一点就好了。:)也许有一些直接修改inode的方法,或者其他什么方法...


5
你在补充材料中的测试并不安全 - 存在竞态条件。考虑如果检查先运行(并且Alpha存在),然后在第二个进程删除Alpha时切换,再切回继续运行,此时Alpha已经消失会发生什么。 - Oddthinking
16个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
65
最终的解决方案是将符号链接和重命名方法结合起来使用:
mkdir alpha_real
ln -s alpha_real alpha

# now use "alpha"

mkdir beta_real
ln -s beta_real tmp 

# atomically rename "tmp" to "alpha"
# use -T to actually replace "alpha" instead of moving *into* "alpha"
mv -T tmp alpha
当然,访问alpha的应用程序必须能够处理路径中符号链接的更改。

11
请参考http://rcrowley.org/2010/01/06/things-unix-can-do-atomically.html,了解`mv`命令的`-T`标志,该标志允许原子交换两个符号链接。 - PypeBros

20

自Linux 3.15以来,新的renameat2系统调用可以原子交换同一文件系统上的两个路径。然而,甚至还没有glibc包装器,更不用说coreutils访问它的方法了。所以它看起来会像这样:

int dirfd = open(".../base", O_PATH | O_DIRECTORY | O_CLOEXEC);
syscall(SYS_renameat2, dirfd, "alpha", dirfd, "bravo", RENAME_EXCHANGE);
close(dirfd);
system("rm -rf alpha");

当然,你需要做适当的错误处理等等--请参见此代码片段以获取更复杂的renameat2包装器。

话虽如此 - 其他人提到的符号链接解决方案更加简单和可移植,因此除非bravo已经存在且你必须原子地更新它,否则使用符号链接。


2020年更新:自glibc 2.28(发布于2018-08-01(Debian Stretch,Fedora 29))以来,该系统调用的glibc包装器已可用。但是,它仍无法通过coreutils访问。

int dirfd = open(".../base", O_PATH | O_DIRECTORY | O_CLOEXEC);
renameat2(dirfd, "alpha", dirfd, "bravo", RENAME_EXCHANGE);
close(dirfd);
system("rm -rf alpha");

谢谢指出这一点!虽然不够便携,但在需要原子移动符号链接的情况下,这个可以帮到我。 - schieferstapel

19

如果您使用符号链接,就可以这样做:

假设alpha是指向目录alpha_1的符号链接,而您想将符号链接切换到指向alpha_2。在切换之前,它看起来像这样:

$ ls -l
lrwxrwxrwx alpha -> alpha_1
drwxr-xr-x alpha_1
drwxr-xr-x alpha_2
要使alpha指向alpha_2,请使用ln -nsf:
$ ln -nsf alpha_2 alpha
$ ls -l
lrwxrwxrwx alpha -> alpha_2
drwxr-xr-x alpha_1
drwxr-xr-x alpha_2

现在您可以删除旧目录:

$ rm -rf alpha_1
请注意,这实际上不是完全原子操作,但由于“ln”命令同时执行取消链接和立即重建符号链接,因此操作非常快速。您可以使用strace验证此行为:
$ strace ln -nsf alpha_2 alpha
...
symlink("alpha_2", "alpha")             = -1 EEXIST (File exists)
unlink("alpha")                         = 0
symlink("alpha_2", "alpha")             = 0
...
你可以根据需要重复这个步骤:例如,当你有一个新版本alpha_3时:
$ ln -nsf alpha_3 alpha
$ rm -rf alpha_2

Linux VFS不支持多个目录硬链接。其他一些*nix系统有限的支持,仅限超级用户使用。您还需要收集所有现在变成孤立的子目录和文件的链接。 - JimB
是的,它应该是一个软链接才能普遍适用。我已经编辑过我的回答了。然而,只要 alpha 始终是一个链接,我就不认为会有任何孤立的文件,这也是我稍微修改问题的意思。当然,您始终需要删除之前版本的目录。 - Doug Currie
非常接近了;结果你还需要使用-n标志,否则你最终会在原始目录下创建一个符号链接。我在发布问题之前实际尝试了你的想法,但它没有起作用,但当我再次查看并注意到-n标志时,它就可以了。另外,对于投票反对你的人,不要理会他们 :) - dirtside
4
A进程试图在alpha中执行某些操作。在此之后,无论你做什么,它可能是原子的或非原子的,你仍然可以在使用时删除该目录。原子性是无用的,你需要的是串行化,而不是原子性,除非访问alpha的代码也是原子的。 - shodanex
7
为什么你接受了这个答案?它显然不是原子性的。 - Navin

18

在这里接受David的解决方案,它是完全原子的...你可能遇到的唯一问题是mv-T选项不是POSIX标准,因此某些POSIX操作系统可能不支持它(FreeBSD,Solaris等...http://pubs.opengroup.org/onlinepubs/9699919799/utilities/mv.html)。稍加修改,这种方法可以改为完全原子,并且可在所有POSIX操作系统上移植。

mkdir -p tmp/real_dir1 tmp/real_dir2
touch tmp/real_dir1/a tmp/real_dir2/a
# start with ./target_dir pointing to tmp/real_dir1
ln -s tmp/real_dir1 target_dir
# create a symlink named target_dir in tmp, pointing to real_dir2
ln -sf tmp/real_dir2 tmp/target_dir
# atomically mv it into ./ replacing ./target_dir
mv tmp/target_dir ./

示例:通过http://axialcorps.wordpress.com/2013/07/03/atomically-replacing-files-and-directories/了解如何原子性地替换文件和目录。


8

使用一个单独的、保证原子性的操作作为信号量。

因此,如果创建和删除文件的操作是原子的:

1)创建一个名为“信号量”的文件。

2)仅当成功创建(与现有文件无冲突)时,执行操作(根据进程要求,执行 alpha 进程或移动目录)

3)删除信号量。


2
只有当任何作用于 alpha 的操作都被改写以首先检查“信号量”并等待能够锁定信号量本身时,这才会有所帮助。如果它们在启动自己的操作时阻止你创建自己的信号量,则也不能行。 - PypeBros
@PypeBros:是的。如果在执行操作之前不检查它,那么它就不能被用作信号量。如果它可以被两个并发进程创建,那么它就不是一个信号量。 - Oddthinking
这是其他答案的很好补充 - 这可能是您唯一能够真正使过程原子化的方法。 - Ken Williams

6

如果你的意思是跨两个操作进行原子操作,我不认为可以做到。最接近的方法可能是:

mv alpha delta
mv bravo alpha
rm -rf delta

但这仍然存在一个小的窗口,其中alpha不存在。

为了最大程度地减少任何尝试在alpha不在场时使用它的可能性,您可以(如果您有权限):

nice --20 ( mv alpha delta ; mv bravo alpha )
rm -rf delta

mv 操作正在进行时,这将显著提高您的进程优先级。

如果像您在补充中所说,只有一个地方检查 alpha 并且如果没有则会出错,您可以更改该代码以不立即报错,而是在短时间内再次尝试(对于两个 mv 操作很容易在子秒级别完成)- 这些重试应该可以缓解任何问题,除非您非常频繁地替换 alpha。


这大概是在shell中能做到的最快的了;你可以编写一个自定义的C程序来移动这两个目录,这将减少一些毫秒级的时间间隔,或者使用Perl脚本(或选择你自己喜欢的语言)。不过重写“rm -fr”没有任何意义。 - Jonathan Leffler

5
这应该可以解决问题:
mkdir bravo_dir alpha_dir
ln -s bravo_dir bravo
ln -s alpha_dir alpha
mv -fT bravo alpha

strace mv -fT bravo alpha 显示:

rename("bravo", "alpha")

在我看来,这看起来非常原子化。


1
这是与两年前David Schmitt发布的完全相同的解决方案。请参见上文。 - Johan Boulé

4

1

mount --bind bravo alpha 在Linux上可以实现此功能。

它会隐藏alpha的内容,但如果您想清除它,可以在其他地方绑定挂载父文件系统。

如果文件系统已经NFS导出,则需要确保导出选项允许跨越文件系统边界,并在服务器上进行绑定挂载。

您还需要注意那些打开了alpha目录或子目录(例如cwd)的进程。

其他*nix可能也有类似的技巧,但这并没有标准化。


1
即使您直接访问i节点,也没有办法在用户空间中原子交换i节点值。

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