mv --exchange:原子交换两个文件或目录
换配置、切发布目录的时候,经常要把两个路径对调。我以前一直是三步写完:
mv file1 file.tmp
mv file2 file1
mv file.tmp file2
能跑,但中间留了个空窗:第一条和第三条之间,file1 这个名字是不存在的。服务恰好在这时候读配置,拿到的就是 ENOENT。窗口很短,短不等于不会撞上。还有更烦的情况,第二条失败的话,会剩一个临时文件和半截状态,得手动收拾。
GNU coreutils 9.5 给 mv 加了 --exchange,一次调用把两个名字换过来:
mv --exchange file1 file2
为什么没有空窗
底层是 renameat2() 加 RENAME_EXCHANGE 标志。内核在同一次系统调用里交换两个目录项,任何时刻两个名字都存在,各自指向一个文件。读的一方要么看到旧内容,要么看到新内容,不会看到文件消失。
先确认工具版本,9.5 起才有:
mv --version
mv --help | grep exchange
版本号不是小数,是按段比的,所以 9.11 比 9.5 新。--help 里 grep 不到这个选项,就说明当前 mv 还不支持。NixOS、Fedora、Debian 13 这类跟得比较紧的发行版一般都有;Ubuntu 22.04 是 8.32,CentOS 7 是 8.22,得自己编一个,或者改用 util-linux 2.40 之后自带的 exch。
两个例子
文件直接换:
mv --exchange a.conf b.conf
目录也一样,但要多加一个 -T。我第一次拿目录试的时候没加,报错了:
$ mv --exchange release-old release-new
mv: cannot exchange 'release-old' and 'release-new/release-old': Unknown error -1
看报错里的第二个路径就明白了:mv 发现最后一个参数是已存在的目录,按老习惯把源目录往里塞,拼成了 release-new/release-old,而这个路径根本不存在。-T(--no-target-directory)让它把最后一个参数当成目标本身,而不是容器:
mv -T --exchange release-old release-new
换的是名字,不是内容,所以目录里有多少文件都一样,一次调用完事。
什么场景值得用
配置切换最顺手。新配置写成 b.conf,确认没问题再和 a.conf 换;旧配置还留在 b.conf 里,真出问题把同一条命令再跑一遍就回滚了,不用额外备份,也不用记改过哪几行。发布目录同理,新版本先解压到独立目录,验证完再和线上目录交换,比改软链指向少一个“链接短暂不存在”的窗口。
限制有两条,都来自系统调用本身。两个路径必须在同一个文件系统上,跨挂载点(比如普通分区和 /dev/shm)会直接返回 EXDEV,不会像普通 mv 那样退化成复制再删除。另外内核和文件系统得认 RENAME_EXCHANGE:ext4、Btrfs、XFS、tmpfs 都支持,网络文件系统和部分 FUSE 实现不一定,上生产前最好在目标机器上试一遍。
顺带说一句
这个时间线挺有意思:renameat2() 在 Linux 3.15(2014 年 6 月)就有了,glibc 到 2.28(2018 年)才封装,命令行这边等 coreutils 9.5(2024 年 3 月)才补上。中间十年,大家一直在写那个三步脚本。老机器上我还是会用回临时文件,但同一块盘上能一次换完的事,没必要再留一个竞态窗口。
本文来自博客园,作者:dashery,转载请注明原文链接:https://www.cnblogs.com/ydswin/p/23570077
浙公网安备 33010602011771号