C语言 在 malloc 中,为什么要使用 brk?为什么不直接使用mmap



malloc的典型实现使用brk/sbrk作为从操作系统声明内存的主要手段。但是,他们也使用mmap来获取大型分配的块。使用brk而不是mmap是否有真正的好处,或者只是传统?用mmap做这一切不是很好吗?

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


作为参考,这里有几个描述 glibcmalloc的文档:

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,源自 dlmalloc,成立于 1987 年。


jemalloc manpage (http://jemalloc.net/jemalloc.3.html) 是这样说的:

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

所以,他们甚至在这里说sbrk是次优的,但他们还是使用它,即使他们已经不厌其烦地编写代码,以便在没有它的情况下工作。

[杰马尔洛克的写作始于2005年。

更新:仔细想想,关于"按偏好顺序"的那一点给了我一条关于询问的线。为什么选择优先顺序?他们只是使用sbrk作为备用mmap以防不受支持(或缺乏必要的功能),还是该过程有可能进入某种状态,可以使用sbrk但不能使用mmap?我会看看他们的代码,看看我是否能弄清楚它在做什么。


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

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

对于通用内存分配器来说,每次mmap(2)调用一次不是一种可行的方法,因为mmap(2)的分配粒度(一次可以分配的最小单个单元)是PAGESIZE(通常为 4096 字节),并且因为它需要缓慢而复杂的系统调用。对于碎片低的小型分配,分配器快速路径应该不需要系统调用。

因此,无论你使用什么策略,你仍然需要支持多个glibc所谓的内存竞技场,GNU手册提到:"多个竞技场的存在允许多个线程在不同的竞技场中同时分配内存,从而提高性能。


jemalloc manpage (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,则块来自主竞技场和主堆。如果此位为 1,则块来自 mmap'd 内存,并且可以从块的地址计算堆的位置。

所以现在的问题是,如果我们已经在使用mmap(2),为什么不在进程mmap(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)没有这些问题,并且明确设计为允许安全地扩展其内存。

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

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

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

这能很好地与realloc()fork()等配合使用吗?

mmap()

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

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

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

明显的优势是你可以就增加最后一个分配,这是你不能用mmap(2)做的事情(mremap(2)是一个Linux扩展,不是可移植的)。

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

我不知道 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中已被弃用,此时应被视为不可移植。如果您正在开发新的分配器,您可能不想依赖它们。

相关内容

  • 没有找到相关文章

最新更新