查看源代码 超级载体
超级载体是在虚拟机启动时分配的大内存区域,在运行时可以从中分配普通载体。
超级载体功能是在 OTP R16B03 中引入的。它通过命令行选项 +MMscs <大小,单位为 Mb> 启用,并且可以使用其他选项进行配置。
问题
此功能的最初动机是客户要求在虚拟机启动时预先分配物理内存供其使用。
其他问题是操作系统对 mmap 实现的各种已知限制
- 随着 mmap 区域数量的增长,mmap/munmap 的性能越来越差。
- mmap 区域之间的碎片问题。
第三个问题是半字模拟器中低内存的管理。该实现使用朴素的线性搜索结构来保存空闲段,当碎片增加时,会导致性能下降。
解决方案
在虚拟机启动时分配一个大的连续地址空间区域,然后在运行时使用该区域来满足我们的动态内存需求。换句话说:实现我们自己的 mmap。
用例
如果命令行选项 +MMscrpm(保留物理内存)设置为 false,则仅在启动时为超级载体分配虚拟空间。然后,超级载体充当“替代 mmap”实现,而不会改变物理内存页的消耗。当从超级载体完成分配时,将按需保留物理页,并在内存释放回超级载体时取消保留。
如果 +MMscrpm 设置为 true(默认值),则初始分配将为整个超级载体保留物理内存。这可以供希望确保虚拟机具有一定最小物理内存量的用户使用。
但是,保留物理内存的实际含义在很大程度上取决于操作系统及其配置方式。例如,Linux 上不同的内存过度提交设置会极大地改变行为。
第三个功能是让超级载体限制虚拟机使用的最大内存量。如果 +MMsco(仅超级载体)设置为 true(默认值),则仅从超级载体完成分配。当超级载体已满时,虚拟机将由于内存不足而失败。如果 +MMsco 为 false,如果超级载体已满,则分配将直接使用 mmap。
实现
整个超级载体实现都保留在 erl_mmap.c 中。该名称表明它可以被视为我们自己的 mmap 实现。
超级载体需要满足两种略有不同的分配请求:多块载体 (MBC) 和单块载体 (SBC)。它们都是相当大的连续内存块,但 MBC 和 SBC 对对齐和大小有不同的要求。
SBC 可以具有任意大小,并且仅需要最小 8 字节对齐。
MBC 的限制更多。它们只能具有一些固定的 2 的幂大小。起始地址需要具有非常大的对齐方式(当前为 256 kb,称为“超级对齐”)。这是一个设计选择,它允许在 MBC 中每个已分配的块的开销非常低。
为了减少超级载体内的碎片,最好将 SBC 和 MBC 分开。具有统一对齐方式和大小的 MBC 可以非常有效地打包在一起。没有对齐要求的 SBC 也可以非常有效地分配在一起。但是,将它们混合在一起可能会导致大量内存浪费,因为我们需要创建大的填充空洞以达到下一个对齐限制。
因此,超级载体包含两个区域。一个区域用于 MBC,从底部向上增长。一个区域用于 SBC,从顶部向下增长。就像一个堆和堆栈相互增长的过程。
数据结构
MBC 区域称为sa,表示超级对齐,而 SBC 区域称为sua,表示超级未对齐。
请注意,超级对齐中的“超级”和超级载体中的“超级”彼此无关。我们可以选择其他命名来避免混淆,例如“元”载体或“巨型”对齐。
+-------+ <---- sua.top
| sua |
| |
|-------| <---- sua.bot
| |
| |
| |
|-------| <---- sa.top
| |
| sa |
| |
+-------+ <---- sa.bot
当取消分配载体时,将在相应区域内创建一个空闲内存段,除非该载体位于最顶端(在 sa
中)或最底端(在 sua
中),在这种情况下,该区域将向下或向上缩小。
我们需要跟踪所有空闲段,以便将它们重用于新的载体分配。最初的想法是使用与跟踪 MBC 内空闲块的机制相同的机制(alloc_util 和不同的策略)。但是,这不会像人们想象的那么直接,并且还会浪费大量内存,因为它使用预先添加的块头。超级载体的粒度是一个内存页(通常为 4kb)。我们希望分配和释放整个页面,并且我们不希望仅为了保存以下页面的块头而浪费整个页面。
相反,我们将有关所有空闲段的元信息存储在与 sa
和 sua
区域分开的专用区域中。每个空闲段都由一个描述符结构 (ErtsFreeSegDesc
) 表示。
typedef struct {
RBTNode snode; /* node in 'stree' */
RBTNode anode; /* node in 'atree' */
char* start;
char* end;
}ErtsFreeSegDesc;
为了找到满足载体分配的最小空闲段(最佳拟合),空闲段以大小排序的树 (stree
) 组织。我们在分配时在此树中搜索。如果找不到足够大小的空闲段,则会扩展区域 (sa
或 sua
)。如果存在两个或多个大小相等的空闲段,则选择地址最低的段用于 sa
,选择地址最高的段用于 sua
。
在载体取消分配时,我们希望与任何相邻的空闲段合并,以形成一个大的空闲段。为此,所有空闲段也按地址顺序排序的树 (atree
) 组织。
因此,我们总共为超级载体保留四个空闲描述符树;两个用于 sa
,两个用于 sua
。它们都使用支持不同排序顺序的同一红黑树实现。
在分配新的 MBC 时,我们首先在 sa
中搜索空闲段,然后尝试提高 sa.top
,然后作为后备,尝试在 sua
中搜索空闲段。当在 sua
中分配 MBC 时,会分配一个较大的段,然后对其进行修剪以获得正确的对齐方式。SBC 的分配搜索以相反的顺序进行。当在 sa
中分配 SBC 时,大小将向上对齐到超级对齐的大小。
空闲描述符区域
如上所述,空闲段的描述符在单独的区域中分配。此区域具有一个常量可配置大小 (+MMscrfsd),默认为 65536 个描述符。在大多数情况下,这应该足够了。如果描述符区域应填满,则将首先直接从操作系统分配新的描述符区域,然后从超级载体中的 sua
和 sa
分配,最后从正在取消分配的内存段本身分配。从超级载体分配空闲描述符区域只是最后的手段,应避免使用,因为它会产生碎片。
半字模拟器
半字模拟器使用超级载体实现来管理其所有术语存储所需的低内存映射。超级载体在这里不能通过命令行选项配置。可以想象使用由高内存分配使用的超级载体的第二个可配置实例,但这尚未实现。