【问题标题】:Finding roots for garbage collection in C在 C 中寻找垃圾收集的根源
【发布时间】:2012-11-27 03:45:25
【问题描述】:

我正在尝试在 C 中实现一个简单的标记和清除垃圾收集器。算法的第一步是找到根。所以我的问题是如何在 C 程序中找到根源?

在使用 malloc 的程序中,我将使用自定义分配器。这个自定义分配器就是将从 C 程序调用的所有内容,并且可能是自定义 init()。

垃圾收集器如何知道程序中所有的指针(根)是什么?另外,给定一个自定义类型的指针,它如何获取其中的所有指针?

例如,如果有一个指针 p 指向一个类列表,它里面有另一个指针.. 说 q。垃圾收集器是怎么知道的,以便标记它?

更新:如果我在初始化时将所有指针名称和类型发送到 GC 会怎么样?同样,也可以发送不同类型的结构,让GC遍历树。这是一个理智的想法还是我疯了?

【问题讨论】:

  • 可能值得一看 hpl.hp.com/personal/Hans_Boehm/gc 和其中的参考资料。
  • @kallikak- 我试着调查它,但它超出了我的想象:(
  • 不确定如何将指针位置“发送”到 GC,但如果您可以将结构附加/嵌入到对象中,则可以在那里存储指针位置信息。 (注意,它只需要一个位数组,以指示哪个单词是或不是指针。)
  • 如果您点击来自@kallikak 的链接,您会发现一个相当不错的descritpion of the algorithm,而且似乎非常易读(一旦您处理了一些行话)。我推荐它。

标签: c garbage-collection boehm-gc gc-roots


【解决方案1】:

首先,C 中的垃圾收集器,没有广泛的编译器和操作系统支持,必须保守,因为您无法区分合法指针和恰好具有看起来像的值的整数一个指针。甚至保守的垃圾收集器也难以实施。就像,真的很难。通常,您需要限制语言以获得可接受的内容:例如,如果指针被隐藏或混淆,则可能无法正确收集内存。如果您分配 100 个字节并且只保留指向分配的第十个字节的指针,您的 GC 不太可能发现您仍然需要该块,因为它不会看到对开头的引用。另一个非常重要的控制约束是内存对齐:如果指针可以位于未对齐的内存上,则收集器的速度可能会降低 10 倍或更差。

要找到根,您需要知道堆栈的开始位置和堆栈的结束位置。请注意复数形式:每个线程都有自己的堆栈,您可能需要考虑到这一点,具体取决于您的目标。要知道堆栈从哪里开始,而无需输入特定于平台的详细信息(无论如何我可能无法提供),您可以在当前线程的 main 函数中使用汇编代码(只是 main 在非线程可执行文件)来查询堆栈寄存器(x86 上的esp,x86_64 上的rsp 仅命名这两个)。 Gcc 和 clang 支持一种语言扩展,可以让您将变量永久分配给寄存器,这对您来说应该很容易:

register void* stack asm("esp"); // replace esp with the name of your stack reg

register 是一个标准语言关键字,大多数时候被当今的编译器忽略,但与asm("register_name") 结合使用,它可以让你做一些讨厌的事情。)

为确保您不会忘记重要的根,您应该将main 函数的实际工作推迟到另一个。 (在 x86 平台上,您也可以查询 ebp/rbp,堆栈帧基指针,但仍然在 main 函数中执行您的实际工作。)

int main(int argc, const char** argv, const char** envp)
{
    register void* stack asm("esp");
    // put stack somewhere
    return do_main(argc, argv, envp);
}

一旦你进入你的 GC 进行回收,你需要查询当前堆栈指针以找到你中断的线程。为此,您将需要特定于设计和/或特定于平台的调用(尽管如果您在同一个线程上执行某些操作,上述技术仍然有效)。

现在开始真正的寻根之旅。好消息:大多数 ABI 将要求堆栈帧在大于指针大小的边界上对齐,这意味着如果您相信每个指针都在对齐的内存上,您可以将整个堆栈视为 intptr_t* 并检查如果里面的任何模式看起来像你的任何托管指针。

显然,还有其他根源。全局变量(理论上)可以是根,结构内的字段也可以是根。寄存器也可以有指向对象的指针。您需要单独考虑可以是根的全局变量(或完全禁止,在我看来这不是一个坏主意),因为自动发现这些变量会很困难(至少,我不知道该怎么做在任何平台上)。

这些根可能导致堆上的引用,如果你不小心,事情可能会出错。

由于并非所有平台都提供malloc 自省(据我所知),因此您需要实现扫描内存的概念——即您的 GC 知道的内存。它至少需要知道每个此类分配的地址和大小。当您获得对其中之一的引用时,您只需扫描它们以查找指针,就像您为堆栈所做的那样。 (这意味着你应该注意你的指针是对齐的。如果你让你的编译器完成它的工作,这通常是这种情况,但你在使用第三方 API 时仍然需要小心)。

这也意味着你不能将可回收内存的引用放到 GC 无法到达的地方。这是最痛苦的地方,也是你需要格外小心的地方。否则,如果您的平台支持 malloc 内省,您可以轻松判断获得指针的每个分配的大小,并确保不会超出它们。

这只是触及了主题的表面。垃圾收集器非常复杂,即使是单线程的。当您将线程添加到组合中时,您会进入一个全新的伤害世界。

Apple 为 Objective-C 语言实现了这种保守的 GC,并将其命名为 libauto。他们开源了它,以及 Mac OS X 的大部分底层技术,你可以find the source here

我只能在这里引用 Hot Licks:祝你好运!


好吧,在我走得更远之前,我忘记了一些非常重要的事情:编译器优化会破坏 GC。如果您的编译器不知道您的 GC,它很可能永远不会在堆栈中放置某些根(仅在寄存器中处理它们),您将错过它们。如果您可以检查寄存器,这对于单线程程序来说并没有太大问题,但对于多线程程序来说,这又是一个巨大的混乱。

还要非常小心分配的可中断性:您必须确保在您返回一个新指针时您的 GC 不会启动,因为它可以在它被分配给根之前以及当您的程序恢复时收集它它会将新的悬空指针分配给您的程序。

以下是解决编辑问题的更新:

更新:如果我将所有指针名称和类型发送到 GC 时怎么样 我初始化它?同样,不同类型的结构也可以 发送以便 GC 可以遍历树。这甚至是一个理智的想法还是我 我快疯了?

我猜你可以分配我们的内存,然后向 GC 注册它,告诉它它应该是一个托管资源。这将解决可中断性问题。但是,请注意您发送给第三方库的内容,因为如果他们保留对它的引用,您的 GC 可能无法检测到它,因为他们不会向您的 GC 注册他们的数据结构。

而且你可能无法在堆栈中使用根。

【讨论】:

  • 很棒的帖子,谢谢。请阅读更新我有一个奇怪的想法。
  • 感谢您更新您的答案@zneak,这肯定有助于我更好地理解。
  • 还有一个问题,如果我在扫描时遇到堆栈中的数据说“123123123”,它也属于分配的内存区域。这并不意味着它是一个指针,对吧?它可以是具有该值的整数。我相信这就是你所说的难以区分和保守的 gc 的意思。
  • 是的,这就是我的意思:如果不了解结构布局,您的 GC 就无法区分指针和整数。这就是为什么您必须假设任何看起来像指针的整数实际上都是指针。执行此操作的垃圾收集器称为“保守”。
  • 是的,他们使用“保守”一词,因为它听起来比“混杂”更好。 ;)
【解决方案2】:

根基本上都是静态的和自动的对象指针。静态指针将链接到加载模块内。必须通过扫描堆栈帧来找到自动指针。当然,您不知道自动指针在堆栈帧中的哪个位置。

一旦你有了根,你需要扫描对象并找到它们里面的所有指针。 (这将包括指针数组。)为此,您需要识别类对象并以某种方式从中提取有关指针位置的信息。当然,在 C 语言中,许多对象不是虚拟的,它们内部也没有类指针。

祝你好运!

添加:一种可以模糊地使您的任务成为可能的技术是“保守的”垃圾收集。由于您打算拥有自己的分配器,因此您可以(以某种方式)跟踪分配大小和位置,因此您可以从存储中挑选任何指针大小的块并询问“这可能是指向我的一个对象的指针吗?”当然,您永远无法确定,因为随机数据可能“看起来像”指向您的某个对象的指针,但您仍然可以通过这种机制扫描一大块存储空间(例如调用堆栈中的帧,或单个对象)并识别它可能处理的所有可能对象。

使用保守的收集器,您不能安全地进行对象重定位/压缩(在移动对象时修改指向对象的指针),因为您可能会意外修改看起来像对象指针但实际上对某些人来说是有意义的数据的“随机”数据应用。但是您可以识别未使用的对象并释放它们占用的空间以供重复使用。通过适当的设计,可以拥有非常有效的非压缩 GC。

(但是,如果您的 C 版本允许未对齐的指针扫描可能会非常慢,因为您必须尝试字节对齐的每一种变化。)

【讨论】:

  • 没有更简单的方法吗?从堆栈帧中提取指针并从 C 结构中提取指针对象听起来像是难题..
  • 是的,这就是标准 C 没有垃圾收集器的原因。
  • (在 Java 中已经够难了,所有东西都是围绕垃圾收集设计的。)
  • 谢谢,我会试试的。请阅读更新我有一个奇怪的想法。
  • 另外,我了解扫描堆栈帧和数据部分以识别类似变量的指针,但是如何从 struct 等自定义对象中提取指针信息?
猜你喜欢
  • 1970-01-01
  • 2012-01-10
  • 2017-06-02
  • 2023-04-03
  • 1970-01-01
  • 2012-01-18
  • 2013-04-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多