malloc
的典型实现使用brk
/sbrk
作为从操作系统声明内存的主要手段。但是,他们也使用mmap
来获取大型分配的块。使用brk
而不是mmap
是否有真正的好处,或者只是传统?用mmap
做这一切不是很好吗?
(注意:我在这里交替使用sbrk
和brk
,因为它们是同一个 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)
呢?特别是如果如引用的那样,有必要跟踪使用了哪种分配类型。有几个原因:
mmap(2)
可能不受支持。sbrk(2)
已经为流程初始化,而mmap(2)
会引入额外的要求。- 正如glibc wiki所说,"如果请求足够大,mmap()用于直接从操作系统请求内存[...]并且一次可以有限制的此类映射数量。
- 分配了
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中已被弃用,此时应被视为不可移植。如果您正在开发新的分配器,您可能不想依赖它们。