我想将一个文件从 A 复制到 B,它可能位于不同的文件系统上。
还有一些额外的要求:
- 副本全有或全无,崩溃时没有部分或损坏的文件 B 留在原处;
- 不要覆盖现有文件 B;
- 不要与同一命令的并发执行竞争,最多一个可以成功。
我认为这很接近:
cp A B.part && \
ln B B.part && \
rm B.part
但是如果 B.part 存在(即使使用 -n 标志), cp 也会违反 3. 随后 1. 如果其他进程“赢得”cp 并且链接到位的文件不完整,则可能会失败。B.part 也可能是一个不相关的文件,但我很高兴在这种情况下不尝试其他隐藏名称就失败了。
我认为 bash noclobber 有帮助,这是否完全有效?有没有办法在没有 bash 版本要求的情况下获得?
#!/usr/bin/env bash
set -o noclobber
cat A > B.part && \
ln B.part B && \
rm B.part
跟进,我知道一些文件系统无论如何都会失败(NFS)。有没有办法检测这样的文件系统?
其他一些相关但不完全相同的问题:
有没有办法将文件和目录从 tempfs 原子移动到 eMMC 上的 ext4 分区
https://rcrowley.org/2010/01/06/things-unix-can-do-atomically.html
rsync
做这项工作。默认情况下会创建一个临时文件O_EXCL
(仅在使用 时才禁用--inplace
),然后renamed
覆盖目标文件。用于--ignore-existing
不覆盖 B(如果存在)。在实践中,我在 ext4、zfs 甚至 NFS 挂载上从未遇到过任何问题。
别担心,
noclobber
是标准功能。您询问了 NFS。这种代码在 NFS 下很可能会中断,因为检查
noclobber
涉及两个单独的 NFS 操作(检查文件是否存在,创建新文件),并且来自两个单独 NFS 客户端的两个进程可能会进入竞争条件,它们都成功(两者都验证B.part
尚不存在,然后都继续成功创建它,结果它们相互覆盖。)对于您正在写入的文件系统是否支持类似
noclobber
原子的东西,并没有真正进行通用检查。您可以检查文件系统类型,是否为 NFS,但这将是一种启发式方法,不一定是保证。像 SMB/CIFS (Samba) 这样的文件系统可能会遇到同样的问题。通过 FUSE 公开的文件系统可能会或可能不会正确运行,但这主要取决于实现。一种可能更好的方法是通过使用唯一的文件名(通过与其他代理合作)来避免
B.part
步骤中的冲突,这样您就不需要依赖noclobber
. 例如,您可以在文件名中包含您的主机名、PID 和时间戳(+可能是一个随机数)。由于在任何给定时间,主机上都应该有一个在特定 PID 下运行的进程,所以这应该保证唯一性。所以其中之一:
或者:
因此,如果两个代理之间存在竞争条件,它们都会继续操作,但最后一个操作将是原子操作,因此要么 B 存在 A 的完整副本,要么 B 不存在。
mv
您可以通过在复制之后和or操作之前再次检查来减少竞态的大小ln
,但那里仍然存在一个小的竞态条件。但是,不管竞争条件如何,B 的内容应该是一致的,假设两个进程都试图从 A 创建它(或从一个有效文件的副本作为源)。请注意,在第一种情况下
mv
,当存在比赛时,最后一个进程是获胜者,因为rename(2)将自动替换现有文件:因此,当时使用 B 的进程很可能会在此过程中看到它的不同版本(不同的 inode)。如果作者只是试图复制相同的内容,而读者只是在消费文件的内容,那可能很好,如果他们为具有相同内容的文件获得不同的 inode,他们也会很高兴。
使用硬链接的第二种方法看起来更好,但我记得在 NFS 上从许多并发客户端的紧密循环中使用硬链接进行实验并计算成功,那里似乎仍然存在一些竞争条件,似乎两个客户端发出硬链接同时行动,同一个目的地,似乎都成功了。(这种行为可能与特定的 NFS 服务器实现 YMMV 有关。)无论如何,这可能是同一种竞争条件,在这种情况下,您最终可能会为同一个文件获得两个单独的 inode编写器之间的并发以触发这些竞争条件。如果您的作者是一致的(都将 A 复制到 B),并且您的读者只消费内容,那可能就足够了。
最后,您提到了锁定。不幸的是,锁定严重缺乏,至少在 NFSv3 中(不确定 NFSv4,但我敢打赌它也不好。)如果你正在考虑锁定,你应该研究分布式锁定的不同协议,可能是带外的实际的文件副本,但这既具有破坏性,又很复杂,而且容易出现死锁等问题,所以我认为最好避免。
有关 NFS 原子性主题的更多背景信息,您可能需要阅读Maildir 邮箱格式,它的创建是为了避免锁定并且即使在 NFS 上也能可靠地工作。它通过在任何地方保留唯一的文件名来做到这一点(所以你甚至不会在最后得到最终的 B 。)
也许对您的特定情况更有趣,Maildir++ 格式扩展了 Maildir 以添加对邮箱配额的支持,并通过在邮箱内自动更新具有固定名称的文件来实现这一点(这样可能更接近您的 B。)我认为 Maildir++ 尝试追加,这在 NFS 上并不安全,但是有一种重新计算方法,它使用与此类似的过程,它作为原子替换是有效的。
希望所有这些指针都会有用!
你可以为此编写一个程序。
用于
open(O_CREAT|O_RDWD)
打开目标文件,读取所有字节和元数据,检查目标文件是否完整,如果不完整,有两种可能,写入不完整
其他进程正在运行相同的程序。
尝试获取目标文件上的打开文件描述锁定。
失败意味着有一个并发进程,当前进程应该存在。
成功意味着最后一次写入崩溃,您应该重新开始或尝试通过写入文件来修复它。
另请注意,最好
fsync()
在关闭文件并释放锁定之前写入目标文件,否则其他进程可能会读取尚未在磁盘上的数据。https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html
这对于帮助您区分同时运行的程序和最后崩溃的操作非常重要。
cp
通过与 一起执行,您将获得正确的结果mv
。这将要么用“A”的新副本替换“B”,要么将“B”保持原样。更新以适应现有
B
:这不是 100% 原子的,但它已经接近了。有一个竞争条件,其中两个都在运行,都同时进入
if
测试,都看到B
不存在,然后都执行mv
.您可以通过在目标目录中创建适当的临时文件,复制该临时文件,然后将临时文件链接到目标来完成此操作,就像您在问题中所做的那样。
这仅依赖于
linkat(2)
目标文件系统的原子性我认为 Rsync 是合适的工具。
您应该使用 rsync -Pahn --checksum /path/from/source /destination/path
但是要小心,您拥有的文件非常大......