5

我曾在我的机器上尝试使用 sbrk(1),然后故意写出测试页面大小,即 4096 字节。但是当我调用 malloc(1) 时,我在访问 135152 字节后得到了 SEGV,这远远超过一页大小。我知道 malloc 是库函数,它依赖于实现,但考虑到它最终会调用 sbrk,为什么它会给出一个以上的页面大小。谁能告诉我它的内部工作?

我的操作系统是 ubuntu 14.04,我的架构是 x86

更新:现在我想知道是否是因为 malloc 将地址返回到一个足够大以容纳我的数据的空闲列表块。但是那个地址可能在堆的中间,这样我就可以继续写,直到达到堆的上限。

4

2 回答 2

8

UNIX 的旧malloc()实现使用sbrk()/brk()系统调用。但是现在,实现使用mmap()and sbrk(). glibc的malloc()实现(这可能是您在 Ubuntu 14.04 上使用的那个)同时使用sbrk()andmmap()以及在您请求时选择使用哪一个来分配通常取决于分配请求的大小,而 glibc 会动态执行。

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

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

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 标准表示写入进程不拥有的内存是未定义的行为。因此,请自担风险——否则,恶魔可能会从你的鼻子里飞出;-)

于 2015-12-13T12:04:09.033 回答
4

malloc出于性能原因,在大块中分配内存。随后的调用malloc可以为您提供大块的内存,而不必向操作系统询问很多小块。这减少了所需的系统调用的数量。

这篇文章

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

请注意,一些现代实现malloc使用mmap而不是brk/sbrk来分配内存,但除此之外,上述情况仍然成立。

于 2015-12-13T10:33:01.100 回答