【问题标题】:Moving a directory atomically以原子方式移动目录
【发布时间】:2010-09-23 08:58:57
【问题描述】:

我在同一个父目录中有两个目录。调用父目录 base 和子目录 alphabravo。我想用 bravo 替换 alpha。最简单的方法是:

rm -rf alpha
mv bravo alpha

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

附录:

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

【问题讨论】:

  • 您在附录中的测试不安全 - 存在竞争条件。考虑一下如果检查首先运行(并且 Alpha 存在)然后在第二个进程删除 Alpha 时将其切换出来,然后再切换回以继续运行,此时 Alpha 丢失了,会发生什么情况。

标签: linux bash atomic


【解决方案1】:

最终的解决方案是结合符号链接和重命名方法:

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 的应用程序必须能够处理路径中更改的符号链接。

【讨论】:

【解决方案2】:

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

假设 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 不支持多目录硬链接。其他一些 *nixes 的支持有限,仅限于超级用户。您还必须收集子目录和文件的所有现在孤立的链接。
  • 是的,应该是软链接才能普遍适用。我已经编辑了我的回复。但是,只要 alpha 始终是一个链接,我不相信会有任何孤儿,这就是我稍微改变问题的意思。当然,您将始终必须删除以前版本的目录
  • 非常接近;原来你也需要 -n 标志,否则你最终会在原始目录下创建一个符号链接。我实际上在发布问题之前尝试了您的想法,但它没有用,但是当我再次查看并注意到 -n 标志时,它做到了。另外,对投票给你的人大便:)
  • 进程 A 尝试在 alpha 中执行某些操作,无论您在此之后所做的是否是原子的,您仍然可以在使用目录时擦除目录。原子是没用的,你需要的是序列化,而不是原子性,除非你的代码访问 alpha 也是原子的。
  • @dirtside 你为什么接受这个答案?它显然不是原子的。
【解决方案3】:

在这里选择 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/

【讨论】:

    【解决方案4】:

    从 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");
    

    (当然,您应该进行适当的错误处理等 - 请参阅 this gist 以获得更复杂的 renameat2 包装器。)

    也就是说 - 其他人提到的符号链接解决方案更容易和可移植,所以除非 bravo 已经存在并且您必须自动更新它,否则请改用符号链接。


    2020 年更新:自 2018 年 8 月 1 日发布的 glibc 2.28(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");
    

    【讨论】:

    • 感谢您指出这一点!虽然不可移植,但在原子移动符号链接还不够的情况下,这对我很有帮助。
    【解决方案5】:

    使用单独的、有保证的原子操作来充当信号量。

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

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

    2) 当且仅当成功(与现有文件不冲突)时,执行操作(进程 alpha 或移动目录,具体取决于进程)

    3) rm 信号量。

    【讨论】:

    • 只有在重新编写任何应该在 alpha 上发生的操作以首先检查semaphore 并等待能够锁定信号量本身时才会有所帮助.. 如果它们阻止您创建当他们开始自己的操作时你的信号量。
    • @PypeBros:是的。如果您在执行操作之前不检查它,则它不会被用作信号量。如果它可以由两个并发进程创建,则它不是信号量。
    • 这是对其他答案的一个很好的补充 - 这可能是您真正使流程原子化的唯一方法。
    【解决方案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”是没有意义的。
    【解决方案7】:

    SQLite 文档部分File Locking and Concurrency in SQLite Version 3 对其升级锁定协议进行了详尽的描述,以控制并发读取、独占写入和崩溃后的回滚。其中一些想法适用于此。

    【讨论】:

      【解决方案8】:

      这应该可以解决问题:

      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")
      

      这对我来说看起来很原子。

      【讨论】:

      • 这与两年前由 David Schmitt 发布的解决方案完全相同。见上文。
      【解决方案9】:

      即使您直接访问 inode,仍然无法原子交换用户空间中的 inode 值。

      【讨论】:

        【解决方案10】:

        担心操作的原子性是没有意义的。问题是,其他任务对 alpha 的访问无论如何都不是原子的。

        Oddthinking 的信号量方法是唯一可行的方法。

        如果您无法修改其他任务,那么您必须在替换之前确保它没有运行。

        【讨论】:

          【解决方案11】:

          我不相信有任何原子方法可以做到这一点。最好的办法是这样做:

          mv alpha delme
          mv bravo alpha
          rm -rf delme
          

          【讨论】:

            【解决方案12】:

            要记住的是,如果您的进程在此移动/删除发生时打开了任何处于 alpha 阶段的文件,则该进程不会注意到,并且当文件关闭并最终删除时,写入的任何数据都将丢失。

            【讨论】:

              【解决方案13】:

              mv 和 ln 可用于原子操作。我已经使用 ln(1) 以原子方式部署 Web 应用程序。

              替换符号链接的正确方法是使用 ln -nsf

              ln -nsf <target> <link_name>
              

              例如

              $ mkdir dir1
              $ mkdir dir2
              $ ln -s dir1 mylink
              $ ls -l mylink
              lrwxrwxrwx  1 phil phil 4 Nov 16 14:45 mylink -> dir1
              $ ln -nsf dir2 mylink
              $ ls -l mylink
              lrwxrwxrwx  1 phil phil 4 Nov 16 14:46 mylink -> dir2
              

              【讨论】:

              • ln -nsf 很快,但它实际上不是原子的。 ln 进程实际上取消链接,然后立即重新创建符号链接。您可以使用 strace 验证这一点。见blog.moertel.com/articles/2005/08/22/…
              【解决方案14】:

              还可以使用unionfs-fuse 以某个前缀(此处为Z)一次替换整个部分内容:

              # mkdir a b c Z
              # touch a/1 b/2 c/3
              # ln -s a X
              # ln -s b Y
              # unionfs X=RW:Y=RW Z
              # shopt -s globstar
              # file **
              a:   directory
              a/1: empty
              b:   directory
              b/2: empty
              c:   directory
              c/3: empty
              X:   symbolic link to a
              Y:   symbolic link to b
              Z:   directory
              Z/1: empty
              Z/2: empty
              # ln -sfn c Y
              # file **/*
              a:   directory
              a/1: empty
              b:   directory
              b/2: empty
              c:   directory
              c/3: empty
              X:   symbolic link to a
              X/1: empty
              Y:   symbolic link to c
              Y/3: empty
              Z:   directory
              Z/1: empty
              Z/3: empty
              # fusermount -u Z
              # rm -r a b c X Y Z
              

              【讨论】:

                【解决方案15】:

                mount --bind bravo alpha 应该在 Linux 上执行此操作

                它隐藏了 alpha 的内容,但如果你想清除它,你可以将父文件系统绑定挂载到别处。

                如果文件系统是 NFS 导出的,您可能需要确保导出选项允许跨越文件系统边界并在服务器上执行绑定挂载。

                您还需要注意在 alpha 目录或子目录(例如 cwd)上具有打开句柄的进程。

                其他 *nix 可能有类似的伎俩,但没有标准化。

                【讨论】:

                  【解决方案16】:

                  你为什么不干点这样的事:

                  rm -rf alpha/*
                  mv bravo/* alpha/
                  rm -rf bravo/
                  

                  这意味着 alpha 中的所有内容都被销毁,alpha 永远 被删除,所有内容都被移动。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2019-08-26
                    • 1970-01-01
                    • 2018-06-11
                    • 2011-08-23
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-08-13
                    • 2013-08-20
                    相关资源
                    最近更新 更多