在循环中使用 fseek / rewind

3
我在代码中遇到一个情况,有一个巨大的函数逐行解析记录,验证并写入另一个文件。如果文件出现错误,则调用另一个函数拒绝该记录并写入拒绝原因。
由于程序中存在内存泄漏,它会崩溃并显示SIGSEGV。从某种意义上讲,“重新启动”文件的一种解决方案是将最后处理的记录写入简单文件中。
为了实现这个目标,在处理循环中需要将当前记录号写入文件。如何确保数据在循环内覆盖文件?
使用fseek在循环中定位到第一个位置/倒带是否会降低性能?
记录数量可能很多,有时高达500K。
谢谢。
编辑:内存泄漏已经修复。重启解决方案被建议作为额外的安全措施和提供跳过n条记录的重启机制的手段。对于没有提及它的问题,我感到抱歉。

如果SIGSEGV故障已经修复,并且实际上很少使用恢复失败后的操作,那么您不需要实现此方法。相反,如果被迫从失败中恢复,请计算已处理的内容(输出)并跳过相应的输入。 - vladr
5个回答

6
当面临这种问题时,您可以采用以下两种方法之一:
1. 您提出的方法:对于读取的每个记录,将记录编号(或输入文件返回的位置)写入单独的书签文件。为确保恢复到离开的地方,以免引入重复记录,必须在每次写操作(书签、输出和拒绝文件)后调用 `fflush`。总的来说,这会使典型情况下的性能显著降低。为了完整起见,请注意您有三种方式将内容写入书签文件: - `fopen(..., 'w') / fwrite / fclose` - 极其缓慢 - `rewind / truncate / fwrite / fflush` - 稍微快一些 - `rewind / fwrite / fflush` - 较快;因为记录编号(或 `ftell` 位置)始终与上一个记录编号(或 `ftell` 位置)一样长或更长,并且会完全覆盖它,所以可以跳过 `truncate`,前提是在启动时截断文件(这回答了您最初的问题)
2. 假设大多数情况下一切顺利,在故障之后恢复时,只需计算已输出的记录数量(正常输出加上拒绝),并从输入文件中跳过相同数量的记录。 - 这会使典型情况下的性能非常快,而在故障后恢复的性能也不会受到显著影响。 - 您无需频繁调用 `fflush`。仅需在切换到拒绝文件写入之前刷新主输出文件,以及在切换回主输出文件写入之前刷新拒绝文件即可(对于 500k 记录的输入,可能需要多次调用 `fflush`)。从输出/拒绝文件中删除最后一行未结束的内容,这样一直到该行之前的所有内容将是一致的。
我强烈建议使用第二种方法。与方法1涉及的写操作相比,任何额外(带缓冲)的读操作所需的开销都非常小(`fflush` 可能需要几毫秒;将其乘以 500k,你就得到了几分钟 - 而在一个含有500k记录的文件中计算行数只需要几秒钟,并且更重要的是,文件系统缓存正在与您一起工作,而不是反过来)。
编辑:想要澄清实现第二种方法所需的确切步骤。
当分别写入输出文件和拒绝文件时,只需要在从一个文件切换到另一个文件时刷新。考虑以下情况作为对执行这些“切换时刷出”的必要性的说明:
假设您向主输出文件写入了1000个记录,然后
您必须先手动清除主输出文件,然后再写1行到拒绝文件中,
接着,您向主输出文件再写入200行,而无需先手动清除拒绝文件,
运行时自动为您刷新了主输出文件,因为您已经在主输出文件的缓冲区中积累了大量数据,即1200条记录。
但是,运行时尚未自动将拒绝文件刷新到磁盘上,因为文件缓冲区仅包含一条记录,这不足以自动刷新。
此时您的程序崩溃了。
您恢复并计算主输出文件中的1200个记录(运行时已经为您刷新了这些记录),但拒绝文件中没有任何记录(未刷新)。
您恢复处理输入文件,从第1201条记录开始,假设您只成功地处理了1200条记录到主输出文件;被拒绝的记录将丢失,第1200个有效记录将重复。
您不希望出现这种情况!
现在考虑在切换输出/拒绝文件后手动刷新:
假设您向主输出文件写入了1000个记录,然后
您遇到一个无效的记录,它属于拒绝文件;最后一条记录是有效的;这意味着您正在切换到写入拒绝文件:在写入拒绝文件之前清除主输出文件,
现在您向拒绝文件写入1行,然后
您遇到一条有效记录,它属于主输出文件;最后一条记录是无效的;这意味着您正在切换到写入主输出文件:在写入主输出文件之前清除拒绝文件,
然后,您向主输出文件再写入200行,而无需先手动清除拒绝文件,
假设运行时没有为您自动刷新任何内容,因为从上次在主输出文件上的手动刷新以来缓冲了200条记录,这不足以触发自动刷新。
此时您的程序崩溃了。
您恢复并计算主输出文件中的1000个有效记录(在切换到拒绝文件之前手动清除了这些记录),以及拒绝文件中的1条记录(在切换回主输出文件之前手动清除)。
您将正确地从第1001条记录开始处理输入文件,即在无效记录之后立即处理第一个有效记录。
您重新处理下一个200个有效记录,因为它们没有被刷新,但您不会丢失任何记录或重复任何记录。
如果您不满意运行时自动刷新之间的时间间隔,则还可以每100或每1000个记录手动刷新。这取决于处理记录是否比刷新更昂贵(如果处理更昂贵,则经常刷新,可能在每个记录之后,否则仅在在输出/拒绝之间切换时刷新。)
从故障中恢复:
打开输出文件和拒绝文件进行“读写”,并开始读取和计数每个记录(例如,在records_resume_counter中),直到达到文件结尾
除非您在输出每个记录后清除,否则您还需要对输出文件和拒绝文件的最后一条记录执行一些特殊处理:
从中断的输出/拒绝文件读取记录之前,请记住您在该输出/拒绝

1
哇,这个纯粹是靠重量赢了! :-) - paxdiablo
非常感谢你的启示!你太棒了!! - prabhu

2
如果你可以修改代码使其将最后处理的记录写入文件,那么为什么不能修复内存泄漏问题呢?
在我看来,解决问题的根本原因比治疗症状更好。
相比于打开/写入/关闭类型的操作,fseek()和fwrite()会降低性能,但降幅不会太大。
我假设你将使用第二个文件存储ftell()值(以便从上次中断处继续执行)。你还应该使用fflush()函数确保数据从C运行库写入到操作系统缓冲区。否则,SEGV将导致值不是最新的。

抱歉,我忘了提到这一点。内存泄漏已经修复。这是客户要求的额外安全措施。 - prabhu
然后只需让客户知道会有处理延迟即可。检查点不是免费操作。您可能需要进行一些基准测试,以让他们知道影响将是什么。 - paxdiablo
他不需要在每个记录处进行检查点,在某些情况下(例如,所有拒绝或无拒绝),根本不需要。输出文件本身中有足够的状态信息。 :) - vladr

2

不需要写出整个记录,每次在开始时调用ftell(),并写入文件指针的位置可能更容易。当您必须重新启动程序时,使用fseek()定位到文件中最后一个写入的位置,并继续执行。

当然,修复内存泄漏是最好的解决方案 ;)


0
如果你为每个记录编写最后处理的位置,这将对性能产生明显影响,因为你需要提交写入(通常是通过关闭文件),然后再次打开文件。换句话说,fseek 是你最不用担心的问题。

fflush(以及可能的fsync)可能比关闭和重新打开文件更快,但我怀疑差别不大。 - paxdiablo

0
我建议停止挖掘更深的坑,直接通过Valgrind运行程序。这样做应该可以消除泄漏问题以及其他问题。

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