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


9

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

还有一些其他要求:

  1. 副本是全部或全部,崩溃时不保留任何部分或损坏的文件B;
  2. 不要覆盖现有文件B;
  3. 不要与并发执行同一命令竞争,最多只能成功。

我认为这很接近:

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

但是3.如果B.part存在(即使带有-n标志),则cp不会违反cp违反。随后,如果其他进程“赢得” cp并且链接到位的文件不完整,则可能1.失败。B.part也可能是不相关的文件,但在这种情况下如果不尝试其他隐藏名称,我很乐意失败。

我认为bash noclobber会有所帮助,这样是否可以充分发挥作用?有没有没有bash版本要求的方法?

#!/usr/bin/env bash
set -o noclobber
cat A > B.part && \
ln B.part B && \
rm B.part

跟进,我知道某些文件系统无论如何都会失败(NFS)。有没有办法检测这种文件系统?

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

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

我的FS上的MV是原子的吗?

有没有办法将文件和目录从tempf原子地移动到eMMC上的ext4分区

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


2
您是否仅关注同一命令的并发执行(例如,锁定在您的工具中就足够了),还是其他外部干扰文件?
迈克尔·荷马

3
“交易”可能会更好
大师

1
工具内部的@MichaelHomer足够好,我认为外部会使事情变得非常困难!如果可能的话,可以使用文件锁定...
Evan Benn

1
@marcelm mv将覆盖现有文件B。mv -n不会通知它已失败。如果B已经存在,则ln(1)rename(2))将失败。
伊万·本

1
@EvanBenn好点!我应该更好地阅读您的要求。(我倾向于需要对现有目标进行原子更新,并牢记在心)
marcelm

Answers:


11

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

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


rsync可能做得很好,但是极其复杂的手册页确实使我感到恐惧。选项意味着其他选项,是彼此不兼容的等
埃文鸭舌

据我所知,Rsync不能满足要求3。尽管如此,它还是一个了不起的工具,您不应回避阅读手册页的麻烦。您也可以尝试github.com/tldr-pages/tldr/blob/master/pages/common/rsync.mdcheat.sh/rsync。(tldr和作弊是旨在与你所说的问题,即帮助两个不同的项目,“手册页TL; DR”;许多常用命令的支持,你会看到显示的最常见的用法。
sitaram

@EvanBenn rsync是一个了不起的工具,值得学习!手册页很复杂,因为它用途广泛。别被吓到了:)
乔什(Josh)

@ sitaram,#3可以使用pid文件解决。一个小脚本,就像这里答案一样。
罗伯特·里德尔

2
这是最好的答案。Rsync是原子文件传输的行业标准入门,并且在各种配置中都可以满足您的所有要求。
wKavey19年


4

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

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


可能更好的方法是B.part通过使用唯一的文件名(通过与其他代理协作)来避免步骤中的冲突,从而使您不必依赖noclobber。例如,您可以在主机名,PID和时间戳(可能是随机数)中添加主机名,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不存在。

您可以通过在复制之后和mvor ln运算之前再次检查来减小种族的大小,但是那里的种族情况仍然很小。但是,不管竞争条件如何,假设两个进程都试图从A(或从有效文件的副本作为源)创建B的内容,则B的内容应保持一致。

请注意,在第一种情况下mv,如果存在种族,则最后一个过程是获胜的过程,因为rename(2)将原子替换现有文件:

如果newpath已经存在,它将被原子替换,这样,尝试访问newpath的另一个进程将找不到它。[...]

如果存在newpath,但由于某种原因操作失败,请rename()确保将newpath实例保留在原处。

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

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

最后,您提到了锁定。不幸的是,至少在NFSv3中严重缺乏锁定(不确定NFSv4,但我敢打赌它也不好。)如果您正在考虑锁定,则应该研究用于分布式锁定的不同协议,可能与实际的文件副本,但这既破坏性,复杂又容易导致死锁等问题,所以我想最好避免。


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

也许你会更有趣您的特定情况下,的Maildir ++格式扩展的Maildir增加对邮箱配额支持和原子更新与固定名称的文件的邮箱内(这样可能会更接近您B.)我想的Maildir ++尝试这样做附加,这在NFS上并不是很安全,但是有一种重新计算方法,它使用与此类似的过程,并且可以作为原子替换有效。

希望所有这些指针将是有用的!


2

您可以为此编写程序。

使用open(O_CREAT|O_RDWD)打开目标文件,读取所有字节和元数据来检查目标文件是一个完整的,如果不是,有两种可能性,

  1. 写不完整

  2. 其他进程正在运行同一程序。

尝试获取目标文件上的打开文件描述锁。

失败意味着存在一个并发进程,当前进程应该存在。

成功意味着最后一次写入失败,您应该重新开始或尝试通过写入文件来修复它。

还要注意,最好fsync()在写入目标文件后再关闭文件并释放锁,否则其他进程可能会读取磁盘上尚未存储的数据。

https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html

这对帮助您区分正在同时运行的程序和最后崩溃的操作很重要。


感谢您提供的信息,我有兴趣自行实现,并且会尝试一下。令我惊讶的是,它不是某些coreutils /类似软件包的一部分!
伊万·本

这种方法无法满足崩溃要求后没有留下任何部分或损坏的文件B。确实,最好使用标准方法将文件复制到临时名称,然后将其移动到位:此移动可以是原子的,而复制则不能。
reinierpost

@reinierpost如果崩溃,但是数据未完全复制,则无论如何都将保留部分复制的数据。但是我的方法将检测到并修复它。移动文件不能是原子的,写入磁盘跨物理扇区的任何数据都不是原子的,但是软件(例如OS文件系统驱动程序,这种方法)可以修复该文件(如果为rw)或报告一致的状态(如果为ro) ,如问题的评论部分所述。同样的问题是关于复制,而不是移动。
炸鱼薯条德里克

我还看到了O_TMPFILE,这可能会有所帮助。(如果在FS上不可用,则应导致错误)
Evan Benn

@Evan您已阅读该文档,还是曾经想过为什么O_TMPFILE依赖于文件系统支持?
炸鱼薯条德里克

0

cp与一起执行,您将获得正确的结果mv。这可以将“ B”替换为新的“ A”,也可以保留“ 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


mv B.tmp B将覆盖先前存在的B. cp A B.tmp将覆盖先前存在的B.tmp,两者均失败。
埃文·本

mv B.tmp B除非cp A B.tmp先运行并返回成功结果代码,否则它将不会运行。怎么会失败?另外,我同意这cp A B.tmp会覆盖现有的内容B.tmp,这是您要执行的操作。在&&这当且仅当第一个正常完成的第二个命令将运行的保证。
kaan

在问题中,成功定义为不覆盖现有文件B。使用B.tmp是一种机制,但也不得覆盖任何现有文件。
埃文·本

我更新了答案。最终,如果在文件可能存在或不存在时需要完全100%的原子性以及多个线程,则每个地方都需要一个独占锁(创建特殊文件或使用数据库,或...),每个人都应遵循此独占锁。复制/移动过程。
kaan

此更新仍将覆盖B.tmp,并在测试和mv之间具有竞争条件。是的,关键是要正确地做事,但希望可能不够好。其他答案表明为什么不需要锁和数据库。
伊万·本
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.