26

malloc使用brk/sbrk作为从操作系统申请内存的主要方式的典型实现。但是,它们也用于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'd heaps。要将块映射到堆,您需要知道适用哪种情况。如果该位为 0,则该块来自主竞技场和主堆。如果该位为 1,则块来自mmap'd 内存,并且堆的位置可以从块的地址计算。

【glibc mallocptmalloc派生自dlmalloc, dlmalloc 始于 1987 年。】


jemalloc手册页(http://jemalloc.net/jemalloc.3.html)有这样的说法:

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

因此,他们甚至在这里说这sbrk是次优的,但他们仍然使用它,即使他们已经费心编写代码以便没有它也能正常工作。

[jemalloc 的编写始于 2005 年。]

更新:再想一想,关于“按优先顺序”的那一点给了我一条询问线。为什么是优先顺序?它们是否只是在不支持(或缺少必要的功能)sbrk的情况下用作后备mmap,或者该过程是否有可能进入某种可以使用sbrk但不能使用的状态mmap?我会看看他们的代码,看看我是否能弄清楚它在做什么。


我问是因为我正在用 C 实现一个垃圾收集系统,到目前为止,我认为没有理由使用除mmap. 不过,我想知道我是否缺少某些东西。

(在我的情况下,我还有一个额外的理由要避免brk,那就是我可能需要malloc在某些时候使用。)

4

5 回答 5

10

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

自 1975 年的 Unix V6 以来,这一直是完全相同的形式。请注意,V6 支持 65,535 字节的用户地址空间。因此,对于管理超过 64K(当然不是 TB)并没有太多考虑。

使用mmap似乎是合理的,直到我开始想知道如何更改或添加垃圾收集可以使用mmap又不重写分配算法。

这可以很好地与realloc(),fork()等一起使用吗?

于 2019-04-19T22:48:00.400 回答
9

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

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

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

于 2019-04-19T22:48:23.537 回答
8

每次内存分配调用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 堆。要将块映射到堆,您需要知道适用哪种情况。如果该位为 0,则该块来自主竞技场和主堆。如果该位为 1,则块来自 mmap 的内存,并且堆的位置可以从块的地址计算。

所以现在的问题是,如果我们已经在 using mmap(2),为什么不在进程开始时分配一个 arenammap(2)而不是 using 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)没有这些问题,并且被明确设计为允许安全地扩展其内存。
于 2019-06-17T10:25:20.543 回答
7

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

对于使用例如的幼稚(和不那么幼稚)的程序realloc(3)。附加到一个字符串,这会转化为 1 或 2 个数量级的速度提升;-)

于 2019-04-19T23:12:10.907 回答
4

我不知道具体关于 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 已被弃用,此时应将其视为不可移植。如果你正在开发一个新的分配器,你可能不想依赖它们。

于 2021-01-04T03:42:45.343 回答