3

我有一个代码中的情况,其中有一个巨大的函数可以逐行解析记录,验证并写入另一个文件。

如果文件中有错误,它会调用另一个拒绝记录并写入拒绝原因的函数。

由于程序中的内存泄漏,它与 SIGSEGV 一起崩溃。从崩溃的地方“重新启动”文件的一种解决方案是将最后处理的记录写入一个简单文件。

为此,需要将处理循环中的当前记录号写入文件。如何确保数据在循环内的文件上被覆盖?

使用 fseek 在循环中首先定位/倒带会降低性能吗?

记录的数量可能很多,有时(高达 500K)。

谢谢。

编辑:内存泄漏已经修复。重新启动解决方案被建议作为一种额外的安全措施和手段,以提供重新启动机制以及 SKIP n 记录解决方案。很抱歉没有早点提及。

4

5 回答 5

6

遇到此类问题时,可以采用以下两种方法之一:

  1. 您建议的方法:对于您阅读的每条记录,将记录号(或输入文件上返回的位置)写出ftell到单独的书签文件中。为确保您准确地从上次中断的地方继续,为了不引入重复记录,您必须fflush在每次写入之后(同时写入bookmark和输出/拒绝文件)。这以及一般的无缓冲写入操作会减慢典型的(无失败) 情景显着。为了完整起见,请注意您可以通过三种方式写入书签文件:
    • fopen(..., 'w') / fwrite / fclose- 极慢
    • rewind / truncate / fwrite / fflush- 稍微快一点
    • rewind / fwrite / fflush-稍微快一点;您可以跳过truncate,因为记录号(或ftell位置)将始终与前一个记录号(或位置)一样长或更长ftell,并且将完全覆盖它,前提是您在启动时截断文件一次(这回答了您的原始问题)
  2. 假设在大多数情况下一切都会顺利;失败后恢复时,只需计算已经输出的记录数(正常输出加上拒绝),然后从输入文件中跳过等量的记录数。
    • 这使典型(无故障)场景保持非常快,而在故障后恢复场景的情况下不会显着影响性能。
    • 您不需要fflush归档,或者至少不需要经常归档。fflush在切换到写入拒绝文件之前,您仍然需要主输出文件,并且fflush在切换回写入主输出文件之前拒绝文件(对于 500k 记录输入,可能需要数百或数千次。)只需删除输出/拒绝文件中最后一个未终止的行,直到该行的所有内容都将是一致的。

我强烈推荐方法#2。与方法 #2 所需的任何额外(缓冲)读取相比,方法 #1(三种可能性中的任何一种)所需要的写入非常昂贵(fflush可能需要几毫秒;将其乘以 500k,你会得到几分钟——同时计算500k 记录文件中的行只需要几秒钟,而且,文件系统缓存正在使用,而不是对你不利。)


编辑 只是想澄清实现方法2所需的确切步骤:

  • 当分别写入输出和拒绝文件时,您只需在从写入一个文件切换到写入另一个文件时刷新。考虑以下场景来说明执行这些 flushes-on-file-switch 的必要性:

    • 假设您将 1000 条记录写入主输出文件,然后
    • 您必须在拒绝文件中写入 1 行,而无需先手动刷新主输出文件,然后
    • 您在主输出文件中再写入 200 行,而无需先手动刷新拒绝文件,然后
    • 运行时会自动为您刷新主输出文件,因为您在主输出文件的缓冲区中积累了大量数据,即 1200 条记录
      • 运行时尚未自动将拒绝文件刷新到磁盘,因为文件缓冲区仅包含一条记录,不足以自动刷新
    • 您的程序此时崩溃
    • 您恢复并计算主输出文件中的 1200 条记录(运行时为您刷新了这些记录),但拒绝文件中的 0(!)条记录(未刷新)。
    • 您继续处理记录 #1201 处的输入文件,假设您只有 1200 条记录成功处理到主输出文件;被拒绝的记录将丢失,第 1200 条有效记录将被重复
    • 你不想要这个!
  • 现在考虑在切换输出/拒绝文件后手动刷新:
    • 假设您将 1000 条记录写入主输出文件,然后
    • 您遇到一条属于拒绝文件的无效记录;最后一条记录有效;这意味着您正在切换到写入拒绝文件:在写入拒绝文件之前刷新主输出文件
    • 您现在将 1 行写入拒绝文件,然后
    • 您遇到一个属于主输出文件的有效记录;最后一条记录无效;这意味着您正在切换到写入主输出文件:在写入主输出文件之前刷新拒绝文件
    • 您在主输出文件中再写入 200 行,而无需先手动刷新拒绝文件,然后
    • 假设运行时没有为您自动刷新任何内容,因为自上次手动刷新主输出文件以来缓冲的 200 条记录不足以触发自动刷新
    • 您的程序此时崩溃
    • 您恢复并计算主输出文件中的 1000 条有效记录(您在切换到拒绝文件之前手动刷新了这些记录),以及拒绝文件中的 1 条记录(您在切换回主输出文件之前手动刷新)。
    • 您正确地恢复处理记录 #1001 处的输入文件,这是紧跟在无效记录之后的第一个有效记录。
    • 您重新处理接下来的 200 条有效记录,因为它们没有被刷新,但是您没有丢失记录,也没有重复
  • 如果您对运行时自动刷新之间的间隔不满意,您也可以每 100 条或每 1000 条记录手动刷新一次。这取决于处理记录是否比刷新更昂贵(如果处理更昂贵,则经常刷新,可能在每条记录之后刷新,否则仅在输出/拒绝之间切换时刷新。)

  • 从失败中恢复

    • 打开输出文件和拒绝文件以进行读取和写入,并从读取和计算每条记录(例如 in records_resume_counter)开始,直到到达文件末尾
    • 除非您在输出每条记录后刷新,否则您还需要对输出和拒绝文件中的最后一条记录执行一些特殊处理:
      • 在从中断的输出/拒绝文件中读取记录之前,请记住您在所述输出/拒绝文件中的位置(使用ftell),我们称之为last_valid_record_ends_here
      • 阅读记录。验证记录不是部分记录(即运行时尚未将文件刷新到记录的中间)。
      • 如果每行有一条记录,则可以通过检查记录中的最后一个字符是否为回车符或换行符(\n或 `r`)来轻松验证
        • 如果记录完整,则增加记录计数器并继续下一条记录(或文件结尾,以先到者为准。)
        • 如果记录是部分的,则fseek返回last_valid_record_ends_here并停止从此输出/拒绝文件中读取;不要增加计数器;继续下一个输出或拒绝文件,除非你已经完成了所有这些
    • 打开输入文件以读取并records_resume_counter从中跳过记录
      • 继续处理并输出到输出/拒绝文件;这将自动附加到您停止读取/计算已处理记录的输出/拒绝文件
      • 如果您必须对部分记录刷新执行特殊处理,您输出的下一条记录将覆盖其上一次运行 (at last_valid_record_ends_here) 的部分信息 - 您将没有重复、垃圾或丢失的记录。
于 2009-03-03T06:03:25.550 回答
2

如果您可以更改代码以使其将最后处理的记录写入文件,为什么不能更改它以修复内存泄漏?

在我看来,解决问题的根本原因而不是治疗症状是一个更好的解决方案。

fseek()并且fwrite()会降低性能,但远不及打开/写入/关闭类型的操作。

我假设您会将ftell()值存储在第二个文件中(这样您就可以从上次中断的地方继续)。您也应该始终fflush()使用该文件,以确保将数据从 C 运行时库写入操作系统缓冲区。否则,您的 SEGV 将确保该值不是最新的。

于 2009-03-03T05:52:38.527 回答
2

与其写出整个记录,在每个记录的开头调用 ftell() 并写入文件指针的位置可能会更容易。当您必须重新启动程序时, fseek() 到文件中最后写入的位置并继续。

当然,修复内存泄漏是最好的;)

于 2009-03-03T05:53:15.560 回答
0

如果您为每条记录写入最后处理的位置,这将对性能产生显着影响,因为您需要提交写入(通常通过关闭文件)然后再次重新打开文件。在其他作品中,fseek 是您最不担心的问题。

于 2009-03-03T05:56:14.307 回答
0

我会停止挖掘更深的洞,而只是通过Valgrind运行程序。这样做应该可以避免泄漏以及其他问题。

于 2009-03-03T06:03:56.457 回答