如何以事务方式复制文件?

如何以事务方式复制文件?

我想将文件从 A 复制到 B,这可能位于不同的文件系统上。

还有一些额外的要求:

  1. 副本要么全有,要么全无,崩溃时不会留下部分或损坏的文件 B;
  2. 不覆盖现有文件B;
  3. 不要与同一命令的并发执行竞争,最多一个可以成功。

我认为这很接近:

cp A B.part && \
ln B B.part && \
rm B.part

但是 3. 如果 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)。有没有办法检测此类文件系统?

其他一些相关但不完全相同的问题:

跨文件系统的近似原子移动?

mv 在我的 fs 上是原子的吗?

有没有办法以原子方式将文件和目录从 tempfs 移动到 eMMC 上的 ext4 分区

https://rcrowley.org/2010/01/06/things-unix-can-do-atomically.html

答案1

rsync做这项工作。默认情况下会创建一个临时文件O_EXCL(仅当您使用时禁用--inplace),然后renamed覆盖目标文件。用于--ignore-existing不覆盖 B(如果存在)。

实际上,我在 ext4、zfs 甚至 NFS 挂载上从未遇到过任何问题。

答案2

不用担心,noclobber是标准功能

答案3

您问的是 NFS。这种代码很可能在 NFS 下崩溃,因为检查noclobber涉及两个单独的 NFS 操作(检查文件是否存在,创建新文件),并且来自两个单独 NFS 客户端的两个进程可能会陷入竞争状态,其中两个进程都成功(两者都验证它B.part尚不存在,然后都继续成功创建它,结果它们互相覆盖。)

实际上并没有对您正在写入的文件系统是否支持原子性之类的东西进行通用检查noclobber。您可以检查文件系统类型,是否是 NFS,但这只是一种启发式方法,并不一定是保证。 SMB/CIFS (Samba) 等文件系统可能会遇到同样的问题。通过 FUSE 公开的文件系统可能会或可能不会正常运行,但这主要取决于实现。


一个可能更好的方法是通过使用唯一的文件名(通过与其他代理合作)来避免步骤中的冲突B.part,这样您就不需要依赖于noclobber.例如,您可以包含主机名、PID 和时间戳(+可能是随机数)作为文件名的一部分。由于在任何给定时间,主机上都应该有一个进程在特定 PID 下运行,因此应该保证唯一性。

所以以下任一:

test -f B && continue  # skip already existing
unique=$(hostname).$$.$(date +%s).$RANDOM
cp A B.part."$unique"
# Maybe check for existance of B again, remove
# the temporary file and bail out in that case.
mv B.part."$unique" B
# mv (rename) should always succeed, overwrite a
# previously copied B if one exists.

或者:

test -f B && continue  # skip already existing
unique=$(hostname).$$.$(date +%s).$RANDOM
cp A B.part."$unique"
if ln B.part."$unique" B ; then
    echo "Success creating B"
else
    echo "Failed creating B, already existed"
fi
# Both cases require cleanup.
rm B.part."$unique"

因此,如果两个代理之间存在竞争条件,它们都会继续执行该操作,但最后一个操作将是原子的,因此要么 B 与 A 的完整副本一起存在,要么 B 不存在。

您可以通过在复制之后和mvorln操作之前再次检查来减少竞争的大小,但仍然存在一个小的竞争条件。但是,无论竞争条件如何,假设两个进程都尝试从 A 创建 B(或从有效文件作为源的副本),B 的内容应该是一致的。

请注意,在第一种情况下mv,当存在竞赛时,最后一个进程是获胜者,因为重命名(2)将自动替换现有文件:

如果新路径已经存在,它将被自动替换,这样就不会出现另一个进程尝试访问的情况新路径会发现它不见了。 [...]

如果新路径存在但由于某种原因操作失败,rename()保证留下一个实例新路径到位。

因此,当时使用 B 的进程很可能在此过程中看到它的不同版本(不同的 inode)。如果编写者只是尝试复制相同的内容,而读者只是使用文件的内容,那可能没问题,如果他们为具有相同内容的文件获得不同的 inode,他们也会很高兴。

第二种方法使用硬链接看起来更好,但我记得在来自许多并发客户端的 NFS 上的紧密循环中进行硬链接实验并计算成功,并且似乎仍然存在一些竞争条件,其中似乎两个客户端同时发出硬链接操作,其中同一个目的地,两人似乎都成功了。 (这种行为可能与特定的 NFS 服务器实现 YMMV 有关。)在任何情况下,这都可能是同一种竞争条件,在数据量很大的情况下,您最终可能会为同一个文件获得两个单独的索引节点。编写者之间的并发来触发这些竞争条件。如果你的作者是一致的(都将 A 复制到 B),并且你的读者只消费内容,那可能就足够了。

最后,您提到了锁定。不幸的是,至少在 NFSv3 中严重缺乏锁定(不确定 NFSv4,但我敢打赌它也不好。)如果您正在考虑锁定,您应该研究不同的分布式锁定协议,可能是带外的实际的文件副本,但这既具有破坏性,又复杂,而且容易出现死锁等问题,所以我认为最好避免这种情况。


有关 NFS 原子性主题的更多背景信息,您可能需要阅读Maildir邮箱格式,它的创建是为了避免锁定并即使在 NFS 上也能可靠地工作。它通过在各处保留唯一的文件名来实现这一点(所以最后你甚至不会得到最后的 B。)

也许对您的具体情况更有趣,Maildir++ 格式扩展 Maildir 以添加对邮箱配额的支持,并通过自动更新邮箱内具有固定名称的文件来实现这一点(这样可能更接近您的 B。)我认为 Maildir++ 尝试附加,这在 NFS 上并不真正安全,但是有一种重新计算方法,它使用与此类似的过程,并且它作为原子替换是有效的。

希望所有这些指示都会有用!

答案4

cp通过与 一起执行 a 将会得到正确的结果mv。这将要么用“A”的新副本替换“B”,要么将“B”保留为以前的样子。

cp A B.tmp && mv B.tmp B

更新以适应现有的B

cp A B.tmp && if [ ! -e B ]; then mv B.tmp B; else rm B.tmp; fi

这不是 100% 原子性,但已经很接近了。有一个竞争条件,其中两个东西正在运行,两者if同时进入测试,都看到B不存在,然后都执行mv.

相关内容