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

Nat*_*C-K 8 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年。]

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


我之所以问是因为我正在用C实现垃圾回收系统,到目前为止,我看不到除之外的任何用途mmap。我想知道是否还有什么我想念的。

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

Bar*_*mar 8

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

并且可用于malloc()您不想需要真实文件来备份内存。1988 年 SunOS/dev/zero为此目的而实施,而在 1990 年代的 HP-UX 实施了该MAP_ANONYMOUS标志。

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

  • 这解释了为什么过去没有使用 `mmap`,但现代版本*确实*使用它,所以我不确定历史是否解释了为什么他们不专门使用它。也许它们最初被编写为仅使用 `brk`,然后添加了 `mmap` 调用作为改进?但是 jemalloc 只能追溯到 2005 年,它同时使用了 `sbrk` 和 `mmap`。 (2认同)

wal*_*lyk 6

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

自1975年Unix V6以来,它的形式完全相同。请注意,V6支持65,535字节的用户地址空间。因此,对于管理超过64K(肯定不是TB)的信息并没有太多想法。

使用mmap直到我开始琢磨着如何改变或增加,垃圾收集器可以使用似乎是合理的mmap没有太重写分配算法。

请问这项工作很好地与realloc()fork()等?


mos*_*svy 5

明显的优势是您可以将最后的分配增加到位,这是您无法做到的mmap(2)mremap(2)是Linux扩展,不可移植)。

对于天真的(不是那么天真的)程序,realloc(3)例如使用。附加到字符串后,转换为1或2个数量级的速度提升;-)

  • 不用考虑得太深:声称一小块连续的虚拟内存(例如1G)会在32位计算机上造成很大的问题。通常仅使用lisp vm,仿真器等来完成此操作。像malloc()这样的库函数的实现会占用大部分可用地址空间,这是不可接受的。 (2认同)
  • Google 建议 Go 的内存分配器完全基于 `mmap` (2认同)

小智 5

调用mmap(2)每个存储器分配一次不是一个通用存储器分配器的可行方法,因为分配粒度(其可以在一个时间被分配的最小个体单元),用于mmap(2)PAGESIZE(通常是4096个字节),并且因为它需要一个缓慢而复杂的系统调用. 具有低碎片的小分配的分配器快速路径应该不需要系统调用。

所以不管你使用什么策略,你仍然需要支持多个glibc所说的内存arenas,GNU手册提到:“多个arenas的存在允许多个线程同时在不同的arenas中分配内存,从而提高性能。”


jemalloc 联机帮助页 ( http://jemalloc.net/jemalloc.3.html ) 有这样的说法:

传统上,分配器使用 sbrk(2) 来获取内存,这不是最理想的,原因有几个,包括竞争条件、碎片增加以及对最大可用内存的人为限制。如果操作系统支持 sbrk(2),则此分配器按优先顺序同时使用 mmap(2) 和 sbrk(2);否则只使用 mmap(2)。

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


应用程序堆(用 sbrk 声明)和 mmap 的使用引入了一些可能不必要的额外复杂性:

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

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

  1. mmap(2) 可能不支持。
  2. sbrk(2)已经为进程初始化,而mmap(2)会引入额外的要求。
  3. 正如glibc wiki所说,“如果请求足够大,mmap() 用于直接从操作系统[...] 请求内存,并且一次可以有多少这样的映射可能会有限制。”
  4. 分配的内存映射mmap(2)不能那么容易地扩展。Linux 有mremap(2),但它的使用将分配器限制为支持它的内核。预映射许多具有PROT_NONE访问权限的页面会使用过多的虚拟内存。使用MMAP_FIXED取消映射任何可能在没有警告的情况下之前存在的映射。sbrk(2)没有这些问题,并且明确设计为允许安全地扩展其内存。


小智 5

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

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

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