2

我正在使用非常 .NET 风格的方法为 C++ 构建内存管理器。在这样做时,我需要知道哪些对象被认为是可达的;如果可到达对象具有相关对象的句柄,则该对象被认为是可到达的。所以这就提出了哪些对象是我们搜索的根的问题?答案是这些“eve”对象在堆栈上,无论是以托管对象句柄的形式,还是本身具有托管对象句柄的作用域本地对象的实例的形式。

我已经阅读了一些关于这方面的文章,还查看了 MSDN 上关于 Win32 API 中的 StackWalk 方法的实现细节。

一如既往地非常感谢任何帮助。并且请不要建议不要制作内存管理器,或者建议诸如智能指针之类的替代方案。我完全明白我在做什么。谢谢!

4

1 回答 1

3

您的需求有点类似于我目前正在处理的一个小项目,但我的目标不是制作内存管理器,我的目标是检测 dmalloc(以及它所在的调试模式长时间运行的应用程序)正在运行)能够定期停止执行并扫描内存以查找没有引用的堆分配。有点像“愚蠢”的垃圾收集器,但不是以释放内存为目标;相反,目的是记录泄漏的分配以供以后分析(以及在分配时捕获的堆栈跟踪,我已经将其添加到 dmalloc)。请注意,作为通用内存管理器的垃圾收集器,这将是一个非常低效的过程,并且需要“很长时间”才能运行(我还没有完成,

无论如何,我假设您的内存管理器将是您应用程序中堆内存的唯一来源?并且您系统中的线程在完全共享内存环境中运行,其中没有线程有任何内存,包括堆栈空间和线程本地存储空间,这是其他线程无法看到的?如果是这样的话...

我相信只有四类内存,您可以在其中找到指向堆分配的指针:

  1. 在每个线程的调用栈上
  2. 在堆分配本身内
  3. 在静态分配的可写内存中(.bss & .data/.sdata,但不是 .rdata/.rodata)
  4. 在每个线程的线程本地存储空间中

您已经知道指向堆分配的指针可能出现在堆栈上。指向分配的指针也可以(可以代替) 存储在堆对象本身中,甚至不存储在堆栈中。您的问题表明您可能希望将堆栈用作垃圾收集器搜索的“根”;我认为这意味着您希望能够将堆栈上的指针向外跟踪到其他分配,通过内存从一个对象搜索到另一个对象,直到您遍历内存中的所有对象并找到指向所有分配的所有指针。“根”指针也可能存在于静态分配的对象中,可以直接引用它,甚至没有指向堆栈上此类对象的指针,因此您不能假设所有分配都可以从您在堆。此外,不幸的是,对于 C++,除非您能够知道每个分配的结构(如果没有编译器的帮助,您将不会知道),你必须假设任何位置都可能是一个指针。因此,您必须扫描这四类内存中的每一个,寻找指向所有现有分配的潜在指针,如果您在内存中找到与分配地址匹配的值,则用“可能仍在使用”标志标记每一个,不管它是否真的是一个指针。当您扫描内存时,在每个字节位置(或在每个字节位置可被 sizeof(void*) 整除,如果您知道您的平台不能有未对齐地址的指针),您将不得不搜索分配列表查看该值是否在您的分配列表中。如果您在内存中找到与分配地址匹配的值,则使用“可能仍在使用”标记每个标记,无论它实际上是否是指针。当您扫描内存时,在每个字节位置(或在每个字节位置可被 sizeof(void*) 整除,如果您知道您的平台不能有未对齐地址的指针),您将不得不搜索分配列表查看该值是否在您的分配列表中。如果您在内存中找到与分配地址匹配的值,则使用“可能仍在使用”标记每个标记,无论它实际上是否是指针。当您扫描内存时,在每个字节位置(或在每个字节位置可被 sizeof(void*) 整除,如果您知道您的平台不能有未对齐地址的指针),您将不得不搜索分配列表查看该值是否在您的分配列表中。

由于您确信自己知道自己在做什么,因此您的内存管理器可能会在平衡的树结构(可能是红黑树或安德森树)中跟踪这些分配,从而为您提供 O(log n) 插入和查找这些分配,但是导航这些树的比例常数会真正降低垃圾收集器的性能。在进行垃圾收集扫描之前,您需要将树的分配指针按顺序(即使用中序遍历升序或降序)复制到一个平坦的连续缓冲区(即“数组”)中。我建议void*每个分配地址的数组和一个单独的位数组(不是bool数组),每个分配一个位,初始化为全零,如果您发现一个分配的对应位被设置为 1,则它的潜在引用。这仍然会在您扫描垃圾收集时为您提供 O(log n) 查找(使用二进制搜索),但您的查找具有更易于管理的比例常数;此外,这种更紧凑的数据结构往往比平衡树具有更好的缓存命中性能。

现在我将讨论您必须扫描的三类内存中的每一个:

  • 每个线程的调用栈

为此,您必须能够向线程管理器查询每个线程堆栈的顶部和底部。如果您只能获取每个线程的当前堆栈指针,那么您可以使用“回溯”API 来获取该堆栈上的函数返回地址列表。从那里,您可以向后扫描每个堆栈的基数(您不知道),按顺序勾选每个返回地址,直到到达最后一个返回地址,然后您就可以找到堆栈基数(或足够接近) . 对于“当前线程”,请确保不包含任何与您的内存管理器相关的堆栈帧;即,备份一些堆栈帧并忽略与您的垃圾收集器相关的那些,否则您可能会在垃圾收集器的局部变量中找到泄漏分配的地址并将它们误认为

  • 在堆分配本身内

堆对象可以相互引用,并且您可以拥有一个泄漏对象网络,这些对象都相互引用,但是作为一个组,它们被泄漏了。您不想看到它们彼此的指针并将它们视为“正在使用”,因此您必须小心处理这些......并且最后。完成所有其他类别后,您可以折叠/拆分void*分配地址的平面数组,制作单独的“考虑使用中”分配和“尚未验证”分配列表。扫描“考虑使用中”的分配,寻找指向仍在“尚未验证”列表中的分配的潜在指针。当您找到任何内容时,将它们从“尚未验证”列表移到“考虑使用中”列表的末尾,以便您

  • 在静态分配的可写内存中(.bss & .data/.sdata,但不是 .rdata/.rodata)

为此,您需要从链接器获取符号到每个部分的开始和结束(或长度)。如果此类符号尚不存在,或者您无法从平台 API 获取该信息,则需要获取链接器命令脚本(链接器脚本)并对其进行修改以将全局符号添加和初始化到起始地址和结束每个部分的地址(或长度)。.bss 部分包含未初始化的全局、文件范围和类静态数据成员。.data/.sdata 部分包含非常量预初始化全局、文件范围和类静态数据成员。您无需担心 .rdata/.rodata 部分,因为您的程序不会将堆分配地址写入静态 const 数据。

  • 在每个线程的线程本地存储空间中

为此,您必须能够向线程管理器查询每个线程的线程本地存储空间,否则每个线程的启动部分必须将其线程本地存储添加到线程列表中-应用程序的本地空间,并在线程退出时将其删除。


如果您仍然参与并想要这样做,那么现在您可能已经意识到这是一个比您最初想象的更大的项目。让我知道事情的后续!

于 2012-12-24T03:59:18.523 回答