【问题标题】:garbage collection for `fopen()`?`fopen()` 的垃圾收集?
【发布时间】:2019-02-21 00:41:52
【问题描述】:

Boehm gc 只处理内存分配。但是如果想使用垃圾回收来处理fopen(),那么fclose()就不再需要了。有没有办法在 C 中这样做?

附: 例如,PyPy 采用垃圾回收的方式来处理打开文件。

这样做最明显的效果是文件(和套接字等)在超出范围时不会立即关闭。对于为写入而打开的文件,数据可能会在其输出缓冲区中保留一段时间,从而使磁盘上的文件显示为空或被截断。

http://doc.pypy.org/en/latest/cpython_differences.html

【问题讨论】:

  • Ermm .... 我认为答案在 C 中是“不可能的”。即使是保守的 GC。

标签: c garbage-collection fopen pypy boehm-gc


【解决方案1】:

如果不是很明显,没有 Boehm GC 所做的事情在 C 中是可能的。整个库是一大堆未定义的行为,碰巧在某些(很多?)现实世界的实现中起作用. C 实现越先进,尤其是在安全领域,它继续工作的可能性就越小。

话虽如此,我看不出有任何理由不能将相同的原则扩展到FILE* 句柄。然而,问题在于它必然是一个保守的 GC,剩余引用的误报会阻止文件被关闭,这会对进程和文件系统的状态产生明显的影响。但是,如果您在正确的位置明确地fflush,它可能只是半损坏是可以接受的。

另一方面,使用文件描述符绝对没有有意义的方法,因为它们是小整数。对于剩余的引用,您基本上总是会误报。

【讨论】:

    【解决方案2】:

    TL;DR:是的,但是。不仅如此。

    首先要做的事情。由于标准 C 库本身必须在 exit() 函数中自动对打开的文件句柄进行垃圾收集(请参阅下面的标准引号),因此没有必要调用 fclose,只要:

    1. 您绝对确定您的程序最终会通过从main() 返回或调用exit() 来终止。

    2. 您不在乎在关闭文件之前经过了多长时间(使写入文件的数据可供其他进程使用)。

    3. 如果关闭操作失败(可能是由于磁盘故障),则不需要通知您。

    4. 您的进程不会打开超过FOPEN_MAX 个文件,并且不会尝试打开同一个文件两次。 (FOPEN_MAX 必须至少为 8 个,但这包括三个标准流。)

    当然,除了非常简单的玩具应用程序之外,这些保证非常严格,尤其是对于打开用于写入的文件。首先,您将如何保证主机不会崩溃或断电(无效条件 1)?所以大多数程序员认为不关闭所有打开的文件是一种非常糟糕的风格。

    同样,可以想象一个只打开文件以供阅读的应用程序。在这种情况下,从不调用fclose 的最严重问题将是最后一个问题,即同时打开文件限制。五是一个很小的数字,尽管大多数系统都有更高的限制,但它们几乎都有限制;如果一个应用程序运行的时间足够长,它不可避免地会打开太多的文件。 (条件 3 也可能是一个问题,尽管并非所有操作系统都施加此限制,并且很少有系统对仅为读取而打开的文件施加限制。)

    事实上,这些正是垃圾收集理论上可以帮助解决的问题。通过一些工作,可以获得垃圾收集器来帮助管理同时打开的文件的数量。 但是...如前所述,有许多但是。这里有一些:

    1. 标准库没有义务使用malloc 动态分配FILE 对象,或者根本没有义务动态分配它们。 (例如,一个只允许打开八个文件的库可能有一个内部静态分配的八个FILE 结构数组。)因此垃圾收集器可能永远看不到存储分配。为了让垃圾收集器参与删除FILE 对象,每个FILE* 都需要包装在一个动态分配的代理(一个“句柄”)中,并且每个接受或返回FILE* 指针的接口都必须是用一个创建代理的包裹。这不是太多的工作,但是有很多接口要包装,并且包装器的使用基本上依赖于源代码修改;如果某些文件是由外部库函数打开的,您可能会发现很难引入 FILE* 代理。

    2. 虽然在删除某些对象之前可以告诉垃圾收集器做什么(见下文),但大多数垃圾收集器库没有提供除内存可用性之外的对象创建限制的接口.垃圾收集器只有知道允许同时打开多少个文件,才能解决“打开文件过多”的问题,但它不知道,也没有办法让你告诉它。因此,您必须安排在即将突破此限制时手动调用垃圾收集器。当然,由于您已经包装了对fopen 的所有调用,根据第 1 点,您可以通过跟踪打开的文件计数或对来自fopen() 的错误指示做出反应,将此逻辑添加到包装器中。 (C 标准没有指定检测此特定错误的可移植机制,但 Posix 表示如果进程打开的文件太多,fopen 应该失败并将errno 设置为EMFILE。Posix 还定义了@987654341 @在所有进程中打开的文件过多的情况下的错误值;可能值得考虑这两种情况。)

    3. 此外,垃圾收集器没有将垃圾收集限制为单一资源类型的机制。 (在标记清除垃圾收集器中实现这一点非常困难,例如 BDW 收集器,因为需要扫描所有使用的内存以找到活动指针。)因此,只要所有文件描述符插槽都用完就触发垃圾收集可以结果是相当昂贵。

    4. 最后,垃圾收集器不保证垃圾会被及时收集。如果没有资源压力,垃圾收集器可能会长时间处于休眠状态,如果您依赖垃圾收集器关闭文件,这意味着文件可以无限期保持打开状态,即使它们是不再使用。因此,省略fclose() 的原始要求列表中的前两个条件继续有效,即使使用垃圾收集器也是如此。

    所以。是的,但是,但是,但是,但是。以下是 Boehm GC 文档推荐的内容(缩写):

    • 必须立即执行的操作……应通过代码中的显式调用来处理。
    • 应在方便时明确管理稀缺的系统资源。仅将 [垃圾收集] 用作难以明确处理的情况的备用机制。
    • 如果稀缺资源由 [垃圾收集器] 管理,则该资源的分配例程(例如打开的文件句柄)应在发现自己缺少资源时强制进行垃圾收集(如果这还不够,则进行两次)。
    • 如果管理极其稀缺的资源(例如,系统上的文件描述符限制为 20 个打开文件),则可能需要引入描述符缓存方案来隐藏资源限制。

    现在,假设您已经阅读了所有这些内容,并且仍然想这样做。其实很简单。如上所述,您需要定义一个代理对象或句柄,其中包含一个FILE*。 (如果您使用像open() 这样的Posix 接口,它使用文件描述符——小整数——而不是FILE 结构,则句柄包含fd。显然,这是一种不同的对象类型,但机制是相同的。 )

    fopen()(或open(),或任何其他返回打开的FILE*s 或文件的调用)的包装器中,动态分配句柄,然后(在 Boehm GC 的情况下)调用GC_register_finalizer 告诉垃圾收集器在资源即将被删除时调用什么函数。几乎所有的 GC 库都有一些这样的功能。在他们的文档中搜索finalizer。这是documentation for the Boehm collector,我从中提取了上面的警告列表。

    在结束 open 调用时要注意避免出现竞争条件。推荐做法如下:

    1. 动态分配句柄。
    2. 将其内容初始化为一个标记值(例如 -1 或 NULL),表示句柄尚未分配给打开的文件。
    3. 为句柄注册一个终结器。终结器函数应在尝试调用 fclose() 之前检查标记值,因此此时注册句柄即可。
    4. 打开文件(或其他此类资源)。
    5. 如果打开成功,重置句柄以使用从打开返回的。如果失败与资源耗尽有关,请触发手动垃圾收集并根据需要重复。 (注意限制对单个打开的包装器执行此操作的次数。有时您需要执行两次,但连续三个失败可能表明存在其他类型的问题。)
    6. 如果最终打开成功,则返回句柄。否则,可选择取消注册终结器(如果您的 GC 库允许)并返回错误指示。

    强制性 C 标准引号

    1. main()返回与调用exit()是一样的

      §5.1.2.2.3(程序终止):(仅适用于托管实现)

      1. 如果main函数的返回类型是与int兼容的类型,则初始调用main函数的返回等效于用main返回的值调用exit函数函数作为它的参数;到达终止main 函数的} 返回值0。
    2. 调用exit() 刷新所有文件缓冲区并关闭所有打开的文件

      §7.22.4.4(退出函数):

      1. 接下来,所有打开的带有未写入缓冲数据的流都被刷新,所有打开的流都被关闭,所有由tmpfile 函数创建的文件都被删除……

    【讨论】:

      猜你喜欢
      • 2011-01-21
      • 1970-01-01
      • 1970-01-01
      • 2018-12-30
      • 1970-01-01
      • 2010-12-13
      • 2012-05-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多