在malloc中,为什么要完全使用brk?为什么不只使用mmap?

时间:2019-04-19 22:35:06

标签: c linux malloc mmap brk

malloc的典型实现使用brk / sbrk作为从OS声明内存的主要方式。但是,他们还使用mmap来获取大容量分配的块。使用brk而不是mmap真的有好处吗?还是仅仅是传统?用mmap完成所有操作是否效果很好?

(注意:我在这里可以交替使用sbrkbrk,因为它们是同一个Linux系统调用brk的接口。)


作为参考,以下是一些描述glibc malloc的文档:

GNU C库参考手册:GNU分配器
https://www.gnu.org/software/libc/manual/html_node/The-GNU-Allocator.html

glibc Wiki:Malloc概述
https://sourceware.org/glibc/wiki/MallocInternals

这些文件所描述的是sbrk用于声明小额分配的主要场所,mmap用于声明次要场所,并且mmap也用于声明用于以下用途的空间大对象(“比页面大得多”)。

同时使用应用程序堆(用sbrk声明)和mmap会带来一些不必要的额外复杂性:

  

已分配竞技场-主竞技场使用应用程序的堆。其他竞技场使用mmap堆。要将块映射到堆,您需要知道哪种情况适用。如果该位为0,则该块来自主区域和主堆。如果该位为1,则该块来自mmap的内存,并且可以从该块的地址计算出堆的位置。

[Glibc malloc源自ptmalloc,而ptmalloc源自于dlmalloc,它始于1987年。]


jemalloc联机帮助页(http://jemalloc.net/jemalloc.3.html)表示:

  

传统上,分配器使用sbrk(2)获得内存,由于一些原因,该内存不是最佳的,包括竞争条件,增加的碎片以及人为限制最大可用内存。如果操作系统支持sbrk(2),则此分配器将按优先顺序使用mmap(2)和sbrk(2);否则,此分配器将使用mmap(2)和sbrk(2)。否则,仅使用mmap(2)。

因此,他们甚至在这里说sbrk并不是最理想的,但是他们仍然使用它,即使他们已经为编写代码而烦恼了,即使没有代码也可以工作。

[jemalloc的编写始于2005年。]

更新:更多地考虑这一点,关于“按优先顺序排列”的内容使我可以进行询问。为什么选择优先顺序?他们是否只是在不支持sbrk(或缺少必要的功能)的情况下使用mmap作为备用,还是该过程可能进入可以使用sbrk的状态?但不是mmap?我将看一下他们的代码,看看是否能弄清楚它在做什么。


我之所以问是因为我正在用C实现垃圾收集系统,到目前为止,除了mmap之外,我没有其他理由使用任何东西。我想知道是否还有什么我想念的。

(就我而言,我还有一个避免使用brk的理由,这是在某个时候我可能需要使用malloc的原因。)

5 个答案:

答案 0 :(得分:5)

系统调用brk()的优点在于,只有一个数据项可以跟踪内存使用情况,而这又与堆的总大小直接相关。

自1975年的Unix V6起,它的形式完全相同。请注意,V6支持65,535字节的用户地址空间。因此,对于管理超过64K的数据并没有太多想法,当然不是TB。

使用mmap似乎是合理的,直到我开始怀疑更改的或附加的垃圾回收如何使用 mmap 重写分配算法。

>

realloc()fork()等一起使用会很好吗?

答案 1 :(得分:4)

一个明显的优点是您可以按原样增加最后一个分配 ,这是mmap(2)不能做到的(mremap(2)是Linux扩展,不是便携式)。

对于使用realloc(3)的幼稚(而非幼稚)程序,例如。附加到字符串后,转换为1或2个数量级的速度提升;-)

答案 2 :(得分:2)

mmap()在早期的Unix版本中不存在。 brk()是当时增加进程数据段大小的唯一方法。带有mmap()的Unix的第一个版本是80年代中期的SunOS,第一个开放源代码的版本是1990年的BSD-Reno。

要在malloc()中使用,您并不需要一个真实的文件来备份内存。为此,SunOS在1988年实现了/dev/zero,在1990年代,HP-UX实现了MAP_ANONYMOUS标志。

现在mmap()的版本提供了多种分配堆的方法。

答案 3 :(得分:2)

对于通用内存分配器,每个内存分配仅调用一次mmap(2)是不可行的,因为mmap(2)的分配粒度(一次可以分配的最小单个单位)为{{ 1}}(通常为4096个字节),因为它需要缓慢而复杂的syscall。具有较小碎片的小型分配器的分配器快速路径应该不需要系统调用。

因此,无论使用何种策略,您仍然需要支持glibc称为内存竞技场的多个功能,而the GNU manual则提到:“多个竞技场的存在允许多个线程在单独的竞技场中同时分配内存,从而提高了性能。”


  

jemalloc联机帮助页(http://jemalloc.net/jemalloc.3.html)表示:

     
    

传统上,分配器使用sbrk(2)获得内存,由于一些原因,该内存不是最佳的,包括竞争条件,增加的碎片以及人为限制最大可用内存。如果操作系统支持sbrk(2),则此分配器将按优先顺序使用mmap(2)和sbrk(2);否则,此分配器将使用mmap(2)和sbrk(2)。否则,仅使用mmap(2)。

  

据我所知,我看不出它们如何适用于PAGESIZE的现代用法。竞争条件由线程原语处理。碎片处理的方式与sbrk(2)分配的内存区域一样。最大可用内存无关紧要,因为mmap(2)应该用于任何较大的分配,以减少碎片并立即在mmap(2)上将内存释放回操作系统。


  

同时使用应用程序堆(用sbrk声明)和mmap会带来一些额外的复杂性,这些不必要的复杂性可能是不必要的:

     
    

已分配竞技场-主竞技场使用应用程序的堆。其他竞技场使用mmap堆。要将块映射到堆,您需要知道哪种情况适用。如果该位为0,则该块来自主区域和主堆。如果该位为1,则该块来自mmap的内存,并且可以从该块的地址计算出堆的位置。

  

所以现在的问题是,如果我们已经在使用free(3),为什么不只在以mmap(2)开始的过程中分配一个竞技场而不是使用mmap(2)?尤其如此,如果如所引用的那样,有必要跟踪使用了哪种分配类型。有几个原因:

  1. sbrk(2)可能不受支持。
  2. mmap(2)已为流程初始化,而sbrk(2)会引入其他要求。
  3. glibc wiki所说,“如果请求足够大,则mmap()用于直接从操作系统请求内存,并且可能有多少这样的限制。一次可以建立映射。“
  4. mmap(2)分配的内存映射无法轻松扩展。 Linux具有mmap(2),但是它的使用将分配器限制为支持它的内核。使用mremap(2)访问权预映射许多页面会占用过多的虚拟内存。使用PROT_NONE会取消映射之前可能没有映射的任何映射。 MMAP_FIXED没有这些问题,并且经过明确设计,可以安全地扩展其内存。

答案 4 :(得分:1)

我不知道 Linux 上的具体细节,但在 FreeBSD 上已经好几年了,现在 mmap 是首选,而 FreeBSD 的 libc 中的 jemalloc 已完全禁用 sbrk()。 brk()/sbrk() 未在内核中在 aarch64 和 risc-v 的较新端口上实现。

如果我对 jemalloc 的历史理解正确的话,它最初是 FreeBSD 的 libc 中的新分配器,然后它被打破并成为可移植的。现在 FreeBSD 是 jemalloc 的下游消费者。很可能它偏爱 mmap() 而不是 sbrk() 源于围绕实现 mmap 接口构建的 FreeBSD VM 系统的特性。

值得注意的是,在 SUS 和 POSIX 中 brk/sbrk 已被弃用,此时应被视为不可移植。如果您正在开发一个新的分配器,您可能不想依赖它们。