【问题标题】:Dynamic memory allocation and memory block metadata动态内存分配和内存块元数据
【发布时间】:2013-05-29 21:52:12
【问题描述】:

我有一个关于动态内存分配的低级问题。 我知道可能有不同的实现,但我需要了解基本思想。

所以,

当现代操作系统内存分配器或等效的内存分配器分配一块内存时,这个块需要被释放。

但是,在此之前,需要存在一些系统来控制分配过程。

我需要知道:

  • 此系统如何跟踪已分配和未分配的内存。我的意思是,系统需要知道已经分配了哪些块以及它们的大小,以便在分配和释放过程中使用这些信息。

现代硬件是否支持此过程,例如分配位或类似的东西? 或者是某种用于存储分配信息的数据结构。 如果有数据结构,它使用的内存与分配的内存相比有多少?

以大块而不是小块分配内存更好吗?为什么?

感谢任何有助于揭示基本实现细节的答案。

如果需要代码示例,C 或 C++ 就可以了。

【问题讨论】:

  • 这感觉像是功课......
  • 如果您需要相关帮助,请展示您的研究成果
  • 我是一名自学成才的程序员。我使用谷歌和书籍。但是找到这样的信息需要相当长的时间。花时间学习一些东西并没有错,但是要学习的东西太多了。这就是我开始这个线程的原因。我 24 岁。

标签: c++ c linux windows unix


【解决方案1】:

“此系统如何跟踪已分配和未分配的内存。”对于带有操作系统的非嵌入式系统,由操作系统负责组织的virtual page table(当然还有硬件TLB 支持)跟踪程序的内存使用情况。

据我所知(如果我弄错了,社区肯定会对我大喊大叫),跟踪个人 malloc() 的大小和位置有一个很好的数字的实现,并且依赖于运行时库。一般来说,无论何时调用malloc(),都会将大小和位置存储在一个表中。每当您调用free() 时,都会查找所提供指针的表条目。如果找到,则删除该条目。如果未找到,则忽略 free()(这也表明可能存在内存泄漏)。

当虚拟页面中的所有malloc() 条目都被释放时,该虚拟页面随后被释放回操作系统(这也意味着free() 并不总是将内存释放回操作系统,因为虚拟页面可能仍然有其他malloc() 条目)。如果给定虚拟页面中没有足够的空间来支持另一个指定大小的malloc(),则从操作系统请求另一个虚拟页面。

嵌入式处理器通常没有操作系统、虚拟页表,也没有多个进程。在这种情况下,不使用虚拟内存。相反,嵌入式处理器的整个内存被视为一个大的虚拟页面(尽管地址实际上是物理地址),并且内存管理遵循与前面描述的类似过程。

Here 是一个类似的堆栈溢出问题,有更深入的答案。

“以大块而不是小块分配内存更好吗?为什么?”根据需要分配尽可能多的内存,不多也不少。编译器优化非常智能,内存管理几乎总是比程序员手动执行的效率更高(即减少内存碎片)。在非嵌入式环境中尤其如此。

Here 是一个类似的堆栈溢出问题,具有更深入的答案(请注意,它与 C 而不是 C++ 相关,但它仍然与此讨论相关)。

【讨论】:

  • 很好的答案,但是我不确定我是否正确理解了这部分:“跟踪单个 malloc() 大小和位置有很多实现,并且取决于编译器。”编译器与 malloc() 有什么关系?你的意思是“分配器依赖”?还是我误会了你?
  • 动态内存分配的实现会对系统产生巨大的影响。存储有关每个分配的数据会不会过大?在最坏的情况下,它可能会使可用的主内存减半。
  • @Xaqq 我认为你是对的,当我说“依赖于编译器”时我错了。我应该说"runtime-library dependent",我会尽快编辑。
  • @VakhtangTevdorashvili 系统需要跟踪所有内存块的位置。分配器的实际实现以一种令人费解的方式深埋在标准库中,人类无法理解,但系统必须确切知道要释放的块(大小和位置,每个分配总共正好 2 个字)释放它。 根据我的解释可能有缺陷,如果你一直将单个单词分配到堆上,有效记忆可能会差一半,但一般来说,这种情况非常罕见而且非常糟糕。
  • @VakhtangTevdorashvili 此外,如果编译器优化某些重复或永久分配以显着降低开销,我不会感到惊讶;编译器非常聪明,不应被低估。我相信对于大多数应用程序来说这是不必要的并且容易出现错误,但是手动管理更大的 malloc 来代替更小的 malloc 可能会提高速度,但我自己没有测试过这些收益。
【解决方案2】:

嗯,实现这一目标的方法不止一种。 我曾经为教育目的编写了一个malloc()(和free())实现。

这是根据我的经验,现实世界的实施肯定会有所不同。

我使用了一个双链表。 调用malloc() 后返回给用户的内存块实际上是一个struct,其中包含与我的实现相关的信息(即nextprev 指针,以及一个is_used 字节)。

因此,当用户请求 N 个字节时,我分配了 N + sizeof(my_struct) 个字节,在块的开头隐藏了 nextprev 指针,并将剩下的内容返回给用户。

当然,对于使用大量小分配的程序来说,这是一个糟糕的设计(因为每个分配占用 N + 2 个指针 + 1 个字节)。

对于现实世界的实现,您可以查看优秀且众所周知的内存分配器的代码。

【讨论】:

    【解决方案3】:

    通常存在两个不同的层。

    一层存在于应用程序级别,通常作为 C 标准库的一部分。这就是您通过mallocfree(或C++ 中的operator new,通常又通常调用malloc)之类的函数调用的方法。这一层负责你的分配,但不知道内存或它来自哪里。

    在操作系统级别的另一层不知道也不关心您的分配。它只维护一个已保留、分配和访问的固定大小内存页面的列表,以及每个页面的映射位置等信息。

    每一层都有许多不同的实现,但一般来说它是这样工作的:
    当您分配内存时,分配器(“应用程序级部分”)会查看它是否在其账簿中的某处可以提供给您的匹配块(如果需要,一些分配器会将更大的块分成两部分)。

    如果它没有找到合适的块,它会从操作系统中保留一个新块(通常比您要求的大很多)。 Linux 上的 sbrkmmap 或 Windows 上的 VirtualAlloc 将是它可能用于实现该效果的典型函数示例。
    除了向操作系统显示意图和生成一些页表条目之外,这几乎没有什么作用。
    然后分配器(从逻辑上讲,在它的书中)根据其正常操作模式将该大区域分成较小的块,找到合适的块并将其返回给您。请注意,此返回的内存甚至不一定作为物理内存存在(尽管大多数分配器将一些元数据写入每个分配单元的前几个字节,因此它们必须预先对页面进行故障处理)。

    与此同时,后台任务会在不可见的情况下将某个进程曾经使用但已被释放的内存页面清零。这种情况一直在发生,只是暂时的,因为迟早会有人请求内存(通常,这就是空闲任务所做的)。

    一旦您第一次访问包含已分配块的页面中的地址,就会产生错误。这个尚不存在的页(它逻辑上存在,只是物理上不存在)的页表条目被替换为对零页池中的页的引用。在没有剩余内存的罕见情况下,例如,如果一直在分配大量内存,则操作系统会换出它认为不会很快被访问的页面,将其归零,然后返回该页面。 现在该页面成为您工作集的一部分,它对应于实际的物理内存,并计入您的进程配额。当您的进程正在运行时,页面可能会被移入和移出您的工作集,或者可能会被移出和移入,因为您超出了某些限制,并且取决于需要多少内存以及访问它的方式。

    一旦您调用free,分配器就会将释放的区域放回其账簿中。它可能告诉操作系统它不再需要内存,但通常这不会发生,因为它并不是真正需要的,保留一点额外内存并重用它会更有效.此外,释放内存可能并不容易,因为通常您分配/取消分配的单元与操作系统使用的单元不直接对应(并且,在sbrk 的情况下,它们需要以正确的方式发生也订购)。

    当进程结束时,操作系统会简单地丢弃所有页表条目并将所有页面添加到空闲任务将清零的页面列表中。因此,物理内存可供下一个请求的进程使用。

    【讨论】:

    • 在我看来,操作系统完成了大部分内存分配。我认为 malloc 使用操作系统分配函数,仅此而已,但运行时库和 malloc 尤其像我所看到的那样做很多管道。这改变了我对程序和编程的看法。太好了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-13
    • 1970-01-01
    • 2020-04-21
    • 2020-04-30
    • 1970-01-01
    相关资源
    最近更新 更多