malloc() 在哪里分配内存?是进程虚拟地址空间的数据段还是堆段?

问题描述

自从我被介绍到 C 后,我就被告知在 C 中动态内存分配是使用 malloc 系列中的函数完成的。我还了解到使用 malloc 动态分配的内存是在进程的堆部分分配的。

enter image description here

各种操作系统教科书都说 malloc 涉及系统调用(虽然不总是但有时)将堆上的结构分配给进程。现在假设 malloc 返回指向堆上分配的字节块的指针,为什么它需要系统调用函数的活动记录放置在进程的堆栈部分,由于“堆栈部分”已经是进程虚拟地址空间的一部分,活动记录的推入和弹出,堆栈指针的操作,只需从虚拟地址空间的最高可能地址。它甚至不需要系统调用

现在基于同样的理由,既然“堆部分”也是进程虚拟地址空间的一部分,为什么需要系统调用来分配这一部分的字节块。 malloc 之类的例程可以自行处理“空闲”列表和“已分配”列表。它需要知道的只是“数据部分”的结尾。某些文本说系统调用是“将内存附加到进程以进行动态内存分配”所必需的,但是如果 malloc 在“堆部分”上分配内存,为什么在 {{ 期间完全需要将内存附加到进程1}}?可以简单地取自流程的一部分。

在阅读 Kernighan 和 Ritchie 撰写的“The C Programming Language”[2e] 文本时,我发现了他们对 malloc 函数的实现 [第 8.7 节,第 185-189 页]。作者说:

malloc 根据需要调用操作系统获取更多内存。

这就是操作系统文本所说的,但与我上面的想法相反(如果 malloc 在堆上分配空间)。

由于向系统请求内存是一项相对昂贵的操作,作者不会在每次调用 malloc 时都这样做,因此他们创建了一个函数 malloc,该函数至少请求 morecore单位;这个较大的块根据需要被切碎。基本的空闲列表管理由NALLOC完成。

但问题是作者使用 free 向操作系统询问 sbrk() 中的内存。现在维基百科说:

morecorebrk 是 Unix 和类 Unix 操作系统中使用的基本内存管理系统调用,用于控制分配给数据段的内存量 过程。

哪里

数据段(通常表示为.data)是包含初始化的静态变量,即全局变量和静态局部变量的对象文件或程序的相应地址空间的一部分。

我猜这不是“堆部分”。 [数据段为上图中从下数第二个段,而堆为下数第三个段。]


我完全糊涂了。我想知道到底发生了什么以及这两个概念如何正确?请通过将分散的部分连接在一起来帮助我理解这个概念...

解决方法

在您的图表中,标记为“数据”的部分更准确地称为“静态数据”;编译器会在进程启动时为所有全局变量预先分配此内存。

malloc() 使用的堆是进程数据段的其余部分。在此过程中,最初分配给它的内存很少。如果 malloc() 需要更多内存,它可以使用 sbrk() 来扩展数据段的大小,或者它可以使用 mmap() 在地址空间的其他地方创建额外的内存段。

为什么 malloc() 需要这样做?为什么不简单地让整个地址空间可供它使用?这有历史和现实原因。

历史原因是早期的计算机没有虚拟内存。在进程之间切换时,分配给进程的所有内存都被批量交换到磁盘。所以只分配实际需要的内存页很重要。

实际原因是这对于检测各种错误很有用。如果您曾经因为取消引用未初始化的指针而遇到分段违规错误,那么您已经从中受益。进程的大部分虚拟地址空间并未分配给该进程,这使得未初始化的指针可能指向不可用的内存,并且您在尝试使用它时会出错。

在堆(向上增长)和堆栈(向下增长)之间也有一个未分配的间隙。这用于检测堆栈溢出——当堆栈尝试使用该间隙中的内存时,它会收到一个错误,并转换为堆栈溢出信号。

,

这是完整的 malloc() 标准 C 库规范:

7.22.3.4 malloc 函数

概要

#include <stdlib.h>
void *malloc(size_t size);

说明

malloc 函数为大小为 由大小指定,其值不确定。注意这个需要 与浮点零或空的表示不同 指针常量。

退货

malloc 函数返回一个空指针或一个指向 分配的空间。

就是这样。没有提到堆、堆栈或任何其他内存位置,这意味着获取请求内存的底层机制是实现细节。

换句话说,从 C 的角度来看,您并不关心内存来自哪里。符合标准的实现可以自由地以它认为合适的任何方式实现 malloc(),只要它符合上述规范。

,

有人告诉我,在 C 中动态内存分配是使用 malloc 系列中的函数完成的。我还了解到使用 malloc 动态分配的内存是在进程的堆部分分配的。

这两点都正确。

现在假设 malloc 返回指向在堆上分配的字节块的指针,为什么它需要系统调用。

它需要请求调整堆的大小,使其更大。

...“堆栈部分”已经是进程虚拟地址空间的一部分,激活记录的推送和弹出,堆栈指针的操作,[...] 甚至不需要系统调用。

堆栈段是隐式增长的,是的,但这是堆栈段的一个特殊功能。通常没有数据段的这种隐式增长。 (同样请注意,堆栈段的隐式增长并不完美,正如许多人向 SO 发帖询问为什么他们的程序在将大数组分配为局部变量时会崩溃一样。)

现在基于同样的理由,既然“堆部分”也是进程虚拟地址空间的一部分,那么为什么需要系统调用来在该部分中分配一大块字节。

答案 1:因为一直都是这样。
答案2:因为你想让意外的杂散指针引用崩溃,而不是隐式分配内存。

malloc 根据需要调用操作系统获取更多内存。

这就是操作系统文本所说的,但与我上面的想法相反(如果 malloc 在堆上分配空间)。

同样,malloc 确实在堆上请求空间,但它必须使用显式系统调用才能这样做。

但问题是作者使用 sbrk() 向操作系统询问 morecore 中的内存。现在维基百科说:

brk 和 sbrk 是 Unix 和类 Unix 操作系统中使用的基本内存管理系统调用,用于控制分配给进程数据段的内存量。

不同的人对不同的细分市场使用不同的命名法。 “数据”和“堆”段之间没有太大区别。您可以将堆视为一个单独的段,或者您可以将那些系统调用——那些“在堆上分配空间”的系统调用——视为简单地使数据段变大。这是维基百科文章使用的命名法。


一些更新:

我说过“'数据'和'堆'段之间没有太大区别。”我建议您可以将它们视为单个更通用数据段的子部分。实际上有三个子部分:初始化数据、未初始化数据或“bss”,以及堆。初始化数据具有从程序文件中显式复制的初始值。未初始化的数据从所有位为零开始,因此不需要存储在程序文件中;程序文件说的是它需要多少字节的未初始化数据。然后是堆,它可以被认为是数据段的动态扩展,它从大小 0 开始,但可以在运行时通过调用 brksbrk 进行动态调整。

我说,“您希望意外的杂散指针引用崩溃,而不是隐式分配内存”,并且您询问了这个问题。这是为了响应您的假设,不应要求显式调用 brksbrk 来调整堆的大小,并且您建议堆可以自动、隐式地增长,就像堆栈。但这真的如何运作?

自动堆栈分配的工作方式是,随着堆栈指针的增长(通常是“向下”),它最终会到达一个指向未分配内存的点——您发布的图片中间的蓝色部分。那时,您的程序实际上相当于“分段违规”。但是操作系统注意到违规涉及一个位于现有堆栈正下方的地址,因此它不会在实际分段违规时杀死您的程序,而是快速地使堆栈段变大一点,并让您的程序继续进行,好像什么都没有发生了。

所以我认为你的问题是,为什么不让向上增长的堆段以同样的方式工作?我认为可以编写一个以这种方式工作的操作系统,但大多数人会说这是一个坏主意。

我说过,在堆栈增长的情况下,操作系统注意到违规涉及一个地址“正好低于”现有堆栈,并决定在该点增长堆栈。有一个“就在下面”的定义,我不确定它是什么,但现在我认为它通常是几十或几百 KB。你可以通过编写一个分配局部变量的程序来找出

char big_stack_array[100000];

并查看您的程序是否崩溃。

现在,有时杂散指针引用——否则会导致分段违规风格崩溃——只是堆栈正常增长的结果。但有时是程序做了一些愚蠢的事情的结果,比如写的常见错误

char *retbuf;
printf("type something:\n");
fgets(retbuf,100,stdin);

而传统观点是,您不想(也就是说,操作系统不想)通过自动为其分配内存(在任何随机位置)来呵护像这样的损坏程序在地址空间中,未初始化的 retbuf 指针似乎指向) 使其似乎工作。

如果堆被设置为自动增长,操作系统可能会定义一个与现有堆段“足够接近”的类似阈值。显然,该区域内的杂散指针引用会导致堆自动增长,而超出该范围的引用(更远的蓝色区域)将像以前一样崩溃。该阈值可能必须大于控制自动堆栈增长的阈值。必须编写 malloc 以确保不会尝试将堆增长超过该数量。确实,杂散指针引用——即程序错误——碰巧引用了该区域中未分配的内存,将不会被捕获。 (这是真的,对于今天刚离开堆栈末尾的错误、杂散指针引用会发生什么。)

但是,实际上,malloc 跟踪事物并在需要时显式调用 sbrk 并不难。要求显式分配的成本很小,而允许自动分配的成本——即捕获的杂散指针错误的成本——会更大。这是一组与堆栈增长情况不同的权衡,在堆栈增长情况下,显式测试以查看堆栈是否需要增长 - 必须在每次函数调用时进行的测试 - 将非常昂贵。

最后,还有一个复杂的问题。您发布的虚拟内存布局图——带有漂亮的小堆栈、堆、数据和文本段——很简单,而且可能已经过时了。这些天我相信事情可能会复杂得多。正如@chux 在评论中所写,“您对 malloc() 的理解只是处理分配的多种方式之一。对一个模型的清晰理解可能会阻碍(或帮助)理解多种可能性。”这些复杂的可能性包括:

  • 如果一个程序支持协程或多线程,它可能有多个堆栈段来维护多个堆栈。
  • mmapshm_open 系统调用可能会导致分配额外的内存段,这些段分散在堆和堆栈之间的蓝色区域内的任何位置。
  • 对于大量分配,malloc 可能会使用 mmap 而不是 sbrk 从操作系统获取内存,因为事实证明这可能是有利的。

正如吟游诗人所说:“天地之间,霍雷肖,远比你的哲学所梦想的要多。” :-)