为什么malloc(1)会分配超过一页大小的空间?

5
我在我的机器上使用sbrk(1)并有意地写出越界以测试页面大小(4096字节)。但是当我调用malloc(1)时,在访问135152字节后,即使这远超过一页大小,我仍会得到SEGV。我知道malloc是库函数,实现可能不同,但考虑到它最终调用sbrk,为什么会给出超过一页大小的结果。有人能告诉我它的内部工作原理吗?
我的操作系统是ubuntu 14.04,体系结构是x86。
更新:现在我想知道是否因为malloc返回地址到一个足够大以容纳我的数据的空闲列表块。但是该地址可能在堆的中间,以至于我可以持续写入直到达到堆的上限。

你使用的操作系统和架构是什么? 另外,你确定malloc()调用了sbrk()吗? - Crashworks
我不确定,但我的教授和一些在线资源告诉我。我正在使用Ubuntu 14.04下的x86架构。 - Louis Kuang
2
你应该是指4096,而不是4086。这些“(1)”是什么意思?sbrk是一个系统调用(2),而malloc是一个库函数(3)。 - Tom Zych
嗯,我只能对你的特定malloc()进行推测,但我经常看到人们假设“x库函数在底层调用y”,但这并不一定是真的(比如,也许十年前x()调用了y(),但事情已经改变了)。 另外,你尝试过在malloc()中设置断点,然后逐步执行以查看它调用了什么吗? - Crashworks
问题应该包括您正在使用的操作系统和硬件。 - M.M
1
我认为原因正是您在“更新”下写的那样。 - alain
2个回答

8
早期UNIX的malloc()实现使用sbrk()/brk()系统调用。但是现在,实现使用mmap()sbrk()。glibc的malloc()实现(这可能是您在Ubuntu 14.04上使用的实现)同时使用sbrk()mmap(),当您请求分配时,选择使用哪个通常取决于分配请求的大小,glibc会动态地进行选择。

对于小的分配,glibc使用sbrk(),而对于较大的分配,它使用mmap()。宏M_MMAP_THRESHOLD用于决定这一点。目前,它的默认值为128K。这就解释了为什么您的代码可以分配135152字节,因为它大约是128K。即使您只请求1个字节,您的实现也会分配128K以进行高效的内存分配。因此,在超过此限制之前,不会发生段错误。

您可以使用mallopt()通过更改默认参数来调整M_MAP_THRESHOLD

M_MMAP_THRESHOLD
当使用内存分配函数申请内存时,如果要申请的内存大小大于等于M_MMAP_THRESHOLD的限制(以字节为单位),但又无法从空闲列表中获得,则会使用mmap(2)而不是sbrk(2)来增加程序的堆空间。使用mmap(2)分配内存的显着优势是,分配的内存块始终可以独立地释放回系统。(相比之下,仅当在堆的顶部释放内存时,堆才能被修剪。)另一方面,使用mmap(2)也有一些缺点:被释放的空间不会放置在可供后续分配重用的空闲列表上;因为mmap(2)分配必须对齐到页面,所以可能会浪费内存;内核必须执行昂贵的任务来清零通过mmap(2)分配的内存。权衡这些因素导致M_MMAP_THRESHOLD参数的默认设置为128*1024。
此参数的下限为0。上限为DEFAULT_MMAP_THRESHOLD_MAX: 在32位系统上为512*1024,在64位系统上为4*1024*1024*sizeof(long)。
注意: 今天,glibc默认使用动态mmap阈值。阈值的初始值为128*1024,但是当释放的块大小大于当前阈值且小于或等于DEFAULT_MMAP_THRESHOLD_MAX时,阈值会向上调整为已释放块的大小。当动态mmap阈值生效时,修剪堆的阈值也会动态调整为动态mmap阈值的两倍。如果设置了M_TRIM_THRESHOLD、M_TOP_PAD、M_MMAP_THRESHOLD或M_MMAP_MAX参数,则禁用对mmap阈值的动态调整。
#include<malloc.h>

mallopt(M_MMAP_THRESHOLD, 0);

在调用malloc()之前,你可能会看到不同的限制。其中大部分是实现细节,C标准规定写入进程不拥有的内存是未定义行为,因此你需自行承担风险--否则,恶魔可能从你的鼻子里飞出来;-)

谢谢,你的回答很有帮助。 - Louis Kuang

4

malloc 函数是出于性能考虑在大块内存上分配内存。后续对 malloc 的调用可以从大块中获取内存,而不必请求操作系统提供许多小块内存,这减少了所需的系统调用次数。

来自 这篇文章

当进程需要内存时,使用 brk() 或 sbrk() 系统调用将堆的上限向前移动以创建一些空间。由于系统调用在 CPU 使用方面代价昂贵,更好的策略是调用 brk() 来获取一大块内存,然后根据需要拆分它以获得较小的块。这正是 malloc() 函数所做的。它将许多较小的 malloc() 请求聚合到较少的大型 brk() 调用中。这样做可以显著提高性能。

请注意,一些现代实现的 malloc 函数使用 mmap() 代替 brk()/sbrk() 分配内存,但除此之外,上述内容仍然适用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接