首先,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 注册他们的数据结构。
而且你可能无法在堆栈中使用根。