有人告诉我,在 C 中动态内存分配是使用 malloc 系列中的函数完成的。我还了解到,使用 malloc 动态分配的内存是在进程的堆部分分配的。
两点都对。
现在假设 malloc 返回指向在堆上分配的字节块的指针,为什么它需要系统调用。
它需要请求调整堆的大小,使其更大。
...“堆栈部分”已经是进程的虚拟地址空间的一部分,激活记录的推送和弹出,堆栈指针的操作,[...] 甚至不需要系统调用。
堆栈段是隐式增长的,是的,但这是堆栈段的一个特殊功能。数据段通常没有这种隐式增长。(还要注意,堆栈段的隐式增长并不完美,因为有很多人向 SO 提出问题,询问为什么他们的程序在将巨大的数组分配为局部变量时会崩溃。)
现在基于相同的理由,既然“堆部分”也是进程的虚拟地址空间的一部分,为什么需要系统调用来分配该部分中的一大块字节。
答案1:因为一直都是这样。
答案2:因为您希望意外的杂散指针引用崩溃,而不是隐式分配内存。
malloc 要求操作系统根据需要获取更多内存。
这是操作系统文本所说的,但与我上面的想法相反(如果 malloc 在堆上分配空间)。
同样,malloc
确实请求堆上的空间,但它必须使用显式系统调用才能这样做。
但问题是作者使用 sbrk() 来请求操作系统在 morecore 中的内存。现在维基百科说:
brk 和 sbrk 是 Unix 和类 Unix 操作系统中使用的基本内存管理系统调用,用于控制分配给进程数据段的内存量。
不同的人对不同的部分使用不同的命名法。“数据”和“堆”段之间没有太大区别。您可以将堆视为一个单独的段,或者您可以将那些系统调用——那些“在堆上分配空间”的系统调用——简单地视为使数据段更大。这是维基百科文章使用的命名法。
一些更新:
我说“‘数据’和‘堆’段之间没有太大区别。” 我建议您可以将它们视为单个更通用数据段的子部分。实际上有三个子部分:初始化数据、未初始化数据或“bss”以及堆。初始化数据具有从程序文件中显式复制的初始值。未初始化的数据以所有位为零开始,因此不需要存储在程序文件中;程序文件所说的只是它需要多少字节的未初始化数据。然后是堆,它可以被认为是数据段的动态扩展,它的大小从 0 开始,但可以在运行时通过调用brk
和动态调整sbrk
。
我说,“你希望意外的杂散指针引用崩溃,而不是隐式分配内存”,你问过这个问题。这是对您的假设的回应,即显式调用brk
或sbrk
不应该需要调整堆的大小,以及您建议堆可以像堆栈一样隐式自动增长。但是,这将如何运作,真的吗?
自动堆栈分配的工作方式是,随着堆栈指针的增长(通常是“向下”),它最终会到达指向未分配内存的点 - 您发布的图片中间的蓝色部分。那时,您的程序实际上相当于“分段违规”。但是操作系统会注意到违规涉及到一个位于现有堆栈下方的地址,因此它不会在实际的分段违规时终止您的程序,而是快速使堆栈段变大一点,并让您的程序继续进行,就好像什么都没有发生了。
所以我认为你的问题是,为什么不让向上增长的堆段以同样的方式工作?而且我认为可以编写一个以这种方式工作的操作系统,但大多数人会说这是一个坏主意。
我说过,在堆栈增长的情况下,操作系统会注意到违规涉及到一个地址“就在”现有堆栈的“正下方”,并决定在那个时候增长堆栈。有一个“就在下面”的定义,我不确定它是什么,但这些天我认为它通常是几十或几百千字节。您可以通过编写分配局部变量的程序来查找
char big_stack_array[100000];
并查看您的程序是否崩溃。
现在,有时一个杂散的指针引用——否则会导致分段违规样式崩溃——只是堆栈正常增长的结果。但有时这是因为程序做了一些愚蠢的事情,比如常见的写作错误
char *retbuf;
printf("type something:\n");
fgets(retbuf, 100, stdin);
传统观点是,您不想(即操作系统不想)通过自动为其分配内存(在地址空间中未初始化的retbuf
指针似乎指向的任何随机位置)来溺爱这样一个损坏的程序) 使其看起来有效。
如果堆设置为自动增长,操作系统可能会定义一个与现有堆段“足够接近”的类似阈值。显然,该区域内的杂散指针引用会导致堆自动增长,而超出该区域的引用(更远的蓝色区域)会像以前一样崩溃。该阈值可能必须大于管理自动堆栈增长的阈值。 malloc
必须编写以确保不会尝试使堆增长超过该数量。确实,不会捕获碰巧引用该区域中未分配内存的杂散指针引用(即程序错误)。(这是真的,对于今天刚刚离开堆栈末尾的错误、杂散的指针引用会发生什么。)
但是,实际上,malloc
跟踪事物并sbrk
在需要时显式调用并不难。要求显式分配的成本很小,而允许自动分配的成本——即未捕获的杂散指针错误的成本——会更大。与堆栈增长情况相比,这是一组不同的权衡,其中明确测试以查看堆栈是否需要增长——每次函数调用都必须进行的测试——将非常昂贵。
最后,还有一个复杂的问题。您发布的虚拟内存布局图片(带有漂亮的小堆栈、堆、数据和文本段)是一张简单的并且可能已经过时的图片。这些天来,我相信事情可能要复杂得多。正如@chux 在评论中所写,“您的malloc()
理解只是处理分配的多种方式之一。对一个模型的清晰理解可能会阻碍(或帮助)理解许多可能性。” 这些复杂的可能性包括:
- 如果程序支持协程或多线程,则程序可能有多个堆栈段维护多个堆栈。
- 和系统调用可能会导致分配额外的内存段,分散在堆和堆栈之间的蓝色区域内的任何地方
mmap
。shm_open
- 对于大型分配,
malloc
可能使用mmap
而不是sbrk
从操作系统获取内存,因为事实证明这可能是有利的。
另请参阅为什么 malloc() 可以互换调用 mmap() 和 brk()?
正如吟游诗人所说:“天地间的事物比你的哲学梦想的还要多。” :-)