1

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

在此处输入图像描述

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

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

在浏览 Kernighan 和 Ritchie 的文本“C 编程语言”[2e] 时,我遇到了他们对malloc函数的实现 [第 8.7 节第 185-189 页]。作者说:

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

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

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

但问题是作者sbrk()用来向操作系统请求内存morecore。现在维基百科说:

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

在哪里

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

我猜这不是“堆部分”。[数据段是上图中倒数第二段,堆是倒数第三段。]


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

4

4 回答 4

3

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

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

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

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

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

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

于 2021-07-28T22:37:10.893 回答
2

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

两点都对。

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

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

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

堆栈段是隐式增长的,是的,但这是堆栈段的一个特殊功能。数据段通常没有这种隐式增长。(还要注意,堆栈段的隐式增长并不完美,因为有很多人向 SO 提出问题,询问为什么他们的程序在将巨大的数组分配为局部变量时会崩溃。)

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

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

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

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

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

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

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

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


一些更新:

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

我说,“你希望意外的杂散指针引用崩溃,而不是隐式分配内存”,你问过这个问题。这是对您的假设的回应,即显式调用brksbrk不应该需要调整堆的大小,以及您建议堆可以像堆栈一样隐式自动增长。但是,这将如何运作,真的吗?

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

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

我说过,在堆栈增长的情况下,操作系统会注意到违规涉及到一个地址“就在”现有堆栈的“正下方”,并决定在那个时候增长堆栈。有一个“就在下面”的定义,我不确定它是什么,但这些天我认为它通常是几十或几百千字节。您可以通过编写分配局部变量的程序来查找

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从操作系统获取内存,因为事实证明这可能是有利的。

另请参阅为什么 malloc() 可以互换调用 mmap() 和 brk()?

正如吟游诗人所说:“天地间的事物比你的哲学梦想的还要多。” :-)

于 2021-07-28T22:45:53.433 回答
2

这是标准 C 库规范malloc()的全部内容:

7.22.3.4 malloc 函数

概要

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

描述

malloc 函数为大小由 size 指定且值不确定的对象分配空间。请注意,这不必与浮点零或空指针常量的表示相同。

退货

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

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

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

于 2021-07-28T22:34:28.393 回答
1
  • 并非所有虚拟地址在进程开始时都可用。
  • 操作系统确实维护了一个虚拟到物理的映射,但是(在任何给定时间)只有一些虚拟地址在映射中。读取或写入不在映射中的虚拟地址会导致指令级异常。sbrk在地图中放置更多地址。
  • Stack 就像数据段一样,但具有固定的大小,并且没有类似 sbrk 的系统调用来扩展它。我们可以说没有堆段,只有一个固定大小的栈段和一个可以向上增长的数据段sbrk
  • 您说的堆部分实际上是数据部分的托管(由mallocfree)部分。很明显,与堆管理相关的代码不在操作系统内核中,而是在以 CPU 用户模式执行的 C 库中。
于 2021-08-17T09:03:47.000 回答