动态内存分配和内存块元数据

Dynamic memory allocation and memory block metadata

本文关键字:内存 元数据 分配 动态      更新时间:2023-10-16

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

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

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

I need to know:

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

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

在大块中分配内存比在小块中分配内存好吗?为什么?

任何有助于揭示基本实现细节的答案都是值得赞赏的。

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

"系统如何跟踪已分配和未分配的内存。"对于带有操作系统的非嵌入式系统,由操作系统负责组织的虚拟页表(当然要使用硬件TLB支持)可以跟踪程序的内存使用情况。

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

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

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

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

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

这里是一个类似的堆栈溢出问题,有更深入的答案(注意,它属于C而不是c++,但它仍然与本讨论相关)。

要做到这一点,方法不止一种。我曾经为了教育的目的写了一个malloc()(和free())的实现。

这是我的经验,现实世界的实现肯定是不同的。

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

所以当用户请求N个字节时,我分配N + sizeof(my_struct)字节,隐藏nextprev指针在块的开头,并返回给用户。

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

对于一个真实世界的实现,您可以看一下优秀的和众所周知的内存分配器的代码。

通常有两层。

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

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

每一层都有许多不同的实现,但一般情况下是这样的:
当您分配内存时,分配器("应用程序级部分")会查看它的账簿中是否有匹配的块可以分配给您(如果需要,一些分配器会将较大的块分成两部分)。

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

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

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

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

当进程结束时,操作系统简单地丢弃所有页表项,并将所有页面添加到空闲任务将归零的页面列表中。这样物理内存就可以被下一个进程使用了