在不冒险损坏文件的情况下覆盖一个文件

5
经常有应用程序需要保存文件以便以后加载。最近遇到了一次崩溃,我希望以这样的方式编写操作,以确保我要么有新数据,要么有原始数据,但不会出现损坏的混乱。
我的第一个想法是按以下方式执行(保存名为example.dat的文件):
1. 为目标目录创建唯一的文件名,例如example.dat.tmp。 2. 创建该文件并将数据写入其中。 3. 删除原始文件(example.dat)。 4. 将临时文件重命名(“移动”)到原始文件所在位置(example.dat.tmp-> example.dat)。
然后,在加载时,应用程序可以遵循以下规则:
- 如果没有“example.dat”和“example.dat.tmp”,则首次运行/新项目,因此加载默认值/创建新文件。 - 如果存在“example.dat”但不存在“example.dat.tmp”,则加载example.dat(正常加载情况)。 - 如果存在“example.dat.tmp”,则提供用户可能恢复数据的机会。如果也存在“example.dat”,则不要在未经明确用户同意的情况下覆盖它。
但是,经过一些研究,我发现除了操作系统缓存可能可以通过文件刷新方法进行覆盖外,一些磁盘驱动器仍然会在内部缓存甚至欺骗操作系统说它们已完成,因此第4步可能完成,但写入实际上并没有完成,如果系统崩溃,我就会失去我的数据...
我不确定磁盘问题是否可以由应用程序解决,但是上述一般规则是否正确?我应该保留旧的恢复文件以确保安全性,关于这种事情有什么指导方针(例如可接受的磁盘使用率、用户选择在哪里放置此类文件等)。
此外,我应该如何避免用户和其他程序对“example.dat.tmp”的潜在冲突。我记得有时会从其他软件中看到“〜example.dat”,这是更好的约定吗?

3
重命名文件时无需先删除旧文件。这种方式非常常见,即先将内容写入临时文件,然后将临时文件重命名为原始文件。 - Some programmer dude
3
在文件本身中添加一些检查数据可以验证其完整性。这样的话,如果检查临时文件后发现它没有损坏,就可以自动处理“两个文件都存在”的情况--使用临时文件,否则使用dat文件。在打开文件时,您可以使用FILE_FLAG_WRITE_THROUGH标志来绕过写缓存。 - cdhowie
@cdhowie 这样就足够了吗?文档似乎表明不是,但没有指出还需要什么。 (Windows文档似乎表明它相当于Unix下的O_DSYNC,但为了完整性,您需要O_DSYNC | O_SYNC。) - James Kanze
@JamesKanze 这个页面(http://msdn.microsoft.com/zh-cn/library/cc644950%28VS.85%29.aspx)可能会有所帮助。 - cdhowie
@cdhowie 不完全是这样。问题在于文件不仅仅是文件中的数据,还包括控制信息,告诉系统文件的大小等等。为了事务完整性,每次更改都必须写入。(由于他只关心整个文件,而不是部分写入,在关闭文件之前调用 fsync(fd) 可以在某些 Unix 系统上工作,包括 Solaris 和 Linux——只有在定义了 _POSIX_SYCHRONIZED_ID 时,Posix 才需要它。) - James Kanze
显示剩余3条评论
2个回答

2
如果磁盘驱动器向操作系统报告数据实际上已经存储在磁盘上,但实际上并未存储,那么你无法做太多事情。很多磁盘确实会缓存一定数量的写入,并且报告完成这些写入,但是这些磁盘应该有电池备份,无论如何都要完成物理写入(即使系统崩溃,它们也不会丢失数据,因为它们甚至看不到系统崩溃)。
至于其他方面,你说你已经进行了一些研究,那么你毫无疑问知道不能使用std :: ofstream(也不能使用FILE * )来完成此操作;必须在系统级别进行实际的写操作,并以特殊属性打开文件以确保完全同步。否则,操作可能会在操作系统缓冲区中停留一段时间。据我所知,没有办法确保对重命名操作进行这种同步。(但如果始终保留两个版本:在这种情况下,我通常的约定是将数据写入一个名为"example.dat.new"的文件,然后在写入完成后删除任何名为"example.dat.bak"的文件,将"example.dat"重命名为"example.dat.bak",然后将"example.dat.new"重命名为"example.dat"。考虑到这一点,你应该能够弄清楚发生了什么事情,并找到正确的文件(如果需要交互式进行或者插入带有时间戳的初始行)。

很确定大多数磁盘都具有缓存功能,但没有电池。因此,警告“在设备上关闭Windows写缓存缓冲区刷新”,这归结于一些磁盘驱动器可能会撒谎说实际上已经执行了该操作。 - Will
我认为 .bak 的想法有助于在恢复情况下使其更加清晰。因此,现在我真正想要的是在删除 bak 文件之前,“您确实已经将新的 example.dat 获取到磁盘上了,而不是在某个内部高速缓存中吗?” 我想等待几分钟对于基本上每个磁盘来说都是安全的,甚至可能不需要操作系统刷新命令。这将把真正复杂的解决方案留给文件系统和数据库类型的事情(因为在插入/更新等操作时重写多GB文件可能并不实际)。 - Will
@WillNewbery 我知道的具有缓存功能的磁盘都有电池,并保证所有被磁盘确认的写操作都是持久的。如果不是这种情况,你就无法在专业环境中使用该磁盘。 - James Kanze
@WillNewbery 是的,数据实际上存储在磁盘上。这是由Posix保证的,并且在我所工作的所有专业系统上都有效。 - James Kanze
@WillNewbery 当然,你不会在数据集的中间进行写入;你的写入是针对一个日志文件的,如果出现任何问题,可以通过备份重建数据文件。 - James Kanze
据我所知,大多数台式机和笔记本SATA驱动器都没有电池,并且大多数驱动器都会广告缓存空间,这很可能是终端用户所拥有的。我快速查看了这里的服务器驱动器,它们要么没有缓存,要么没有清楚地宣传它(我猜至少有些,或者至少RAID卡为了livejournal的问题)。我试图多次切断测试工作站的电源(有一个16MB缓存的30英镑硬盘),多文件写入和重命名从未出现过问题,因此如果可能的话,它似乎是一个小概率事件,不是主要关注的问题。 - Will

0

如果有可能其他进程正在执行您所描述的相同协议,那么在编写其替代品时,应锁定实际数据文件。

您可以使用flock进行文件锁定。

至于临时文件名,您可以将进程ID作为其一部分,例如“example.dat.3124”,没有其他同时运行的进程会生成相同的名称。


这并没有回答所问的问题。 - Carey Gregory
实际上是可以的。原帖的标题是“在不损坏文件的情况下覆盖文件”。发帖者提出的协议是可行的,除非同时执行它的进程有可能重叠。 - Curt
3
实际上不是这样。OP非常清楚他的问题不是同时写入,而是在写入过程中操作系统崩溃了。 - James Kanze
获取对某个文件的独占写访问权限并不是什么问题。至于使用进程ID获取文件名,如果它崩溃了怎么办?我是否应该在加载时搜索“example.dat.*”以查看是否有可能是我的应用程序? - Will
@WillNewbery 假设您试图保护的数据文件存在并行访问问题(似乎不是这样,但您应该是可以询问的人)。因为Linux主机托管数据库,包括大型关键数据库,所以必须有一个系统调用来禁用写入磁盘的缓存,否则操作系统将无法成为托管事务性数据库的适当平台。如果没有一种方法来保证您的“预影像”实际上已经完全保存在磁盘上,那么就不会有绝对安全的方法。我正在搜索答案,但没有太多运气... - Curt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接