malloc
的典型实现使用brk
/ sbrk
作为从OS声明内存的主要方式。但是,他们还使用mmap
来获取大容量分配的块。使用brk
而不是mmap
真的有好处吗?还是仅仅是传统?用mmap
完成所有操作是否效果很好?
(注意:我在这里可以交替使用sbrk
和brk
,因为它们是同一个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
的原因。)
答案 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)
?尤其如此,如果如所引用的那样,有必要跟踪使用了哪种分配类型。有几个原因:
sbrk(2)
可能不受支持。mmap(2)
已为流程初始化,而sbrk(2)
会引入其他要求。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 已被弃用,此时应被视为不可移植。如果您正在开发一个新的分配器,您可能不想依赖它们。