Chromium 代码库不是只使用malloc()。
例如:
- 渲染器(Blink)的大部分使用两个自制的分配器,PartitionAlloc 和 BlinkGC(Oilpan)。
- 一些子系统(例如 V8 JavaScript 引擎)可以自主处理内存管理。
- 代码库的各个部分使用诸如SharedMemory或DiscardableMemory之类的抽象,与上述类似,它们有自己的页面级内存管理。
其中PartitionAlloc的实现位于src\base\allocator\partition_allocator目录中,是一个独立的库:
PartitionAlloc.md对这个模块进行了系统的描述。简而言之,PartitionAlloc 是一个针对空间效率、分配延迟和安全性进行优化的内存分配器。
PartitionAlloc.md 文档对里面的技术细节有详细的描述。本文进行概要总结,快速帮助读者掌握PartitionAlloc的设计思想和价值。
PartitionAlloc 设计概览
目标与优势
PartitionAlloc 的核心特性包括:
- 空间效率:优化内存使用,减少碎片。
- 分配延迟:快速路径操作,减少分支预测,适合内联。
- 安全性:防止类型混淆等攻击,保护数据免受溢出影响。
性能优化
- 快速路径:通过减少分支指令,实现高效分配和释放。
- 中央分配器:管理分区的内存,每分区单独锁。
- 线程缓存:预取内存,减少锁竞争,改善局部性。
具体而言,通过减少逻辑的分支(使得CPU执行时流水线高度并发)、快速路径中的操作数量极少(使得快速路径性能极高)、使用线程缓存避免锁的使用(提高线程并发度),三个途径提高性能。
安全性
- 分区隔离:不同分区位于不同地址空间,避免交叉污染。
- 物理内存回收:释放后,物理内存归还OS,地址空间保留。
- 保护页:分区首尾设置保护页,防止溢出。
- 元数据保护:不在对象旁存储,减少溢出风险。
对齐保证
- 基本对齐:默认对齐至
kAlignment
(64位系统16B,32位系统8B)。 - 高级对齐:支持自定义对齐,请求大小向上取整至最接近的2的幂次方。
架构细节
- 内存布局:使用2MiB超级页面,分为多个分区页,首尾为保护页,中间存储元数据。
- 槽跨度:超级页面内逻辑串联的存储单元,用于形成存储桶。
- 空闲列表:链接空闲槽,新分配时配置槽位,避免物理页面预提交错误。
内存布局
- 槽跨度编号提供了一个视觉提示,表明它们的大小(以分区页面为单位)。
颜色提供了槽跨度所属存储桶的视觉提示。 - 尽管图中只展示了五种颜色,但实际上,一个超级页面包含数十个槽跨度,其中一些属于同一个存储桶。
- 持有元数据的系统页面使用一个32字节的
PartitionPageMetadata
结构来追踪每一个分区页面,这个结构可能是一个SlotSpanMetadata
(图中的"v")或者一个SubsequentPageMetadata
(图中的"+")。 - 灰色填充表示保护页(每个超级页面的头部和尾部各有一个分区页)。
- 在某些配置中,PartitionAlloc存储的元数据超过了前部一个系统页面所能容纳的量。这些元数据包括用于StarScan和MTECheckedPtr的位图,它们被降级到了原本可以用于槽跨度的空间的头部。这些位图可能只有一个、两个或者没有,这取决于构建配置、运行时配置以及分配的类型。详情请参阅
SuperPagePayloadBegin()
函数。
空闲列表与槽跨度状态
- 空闲列表指针:存储在槽位开头,易受溢出影响,通过字节序反转编码保护。
- 槽跨度状态:满、空、活跃或已解除提交,优先使用活跃槽,以释放内存。
存储桶管理
- 存储桶结构:每个桶有三个链表管理活动、空和已解除提交的槽跨度。
- 动态调整:根据内存压力,动态转换槽跨度状态,优化内存使用。
总结而言,PartitionAlloc 通过多层次的策略确保了高性能和高安全性,特别注重于内存分配的效率和防止内存越界和地址重用等安全威胁。
题外话,base的内存分配器的架构
背景
内存分配器目标在编译时定义了平台特有的选择,涉及用于服务malloc/new
调用的分配器和额外钩子。相关的构建时标志包括use_allocator_shim
和use_partition_alloc_as_malloc
。
默认情况下,除了iOS(尚未支持)和NaCl(无计划支持)之外的所有平台都启用这些标志。
此外,当使用sanitizer(例如asan、msan等)构建时,分配器和shim层都会被禁用。
层次与构建依赖
分配器目标提供了Windows shim层所需的链接器标志。base
目标(几乎)是唯一依赖于分配器的目标。除了少数在链接单元范围内不直接或间接依赖于base
的可执行文件/动态库之外,没有其他目标应依赖于它。
更重要的是,除/base
之外的任何地方都不应依赖于特定的分配器。
如果需要此类功能依赖,应当通过base
中的抽象来实现(参见/base/memory/
)。
为什么base
依赖于分配器?
因为base
需要提供依赖于实际分配器实现的服务。过去,base
试图表现为分配器无关,依赖由其他层注入。这最终导致了一团乱麻。
有关更多背景信息,请参阅分配器清理文档。
那些在某种程度上依赖于base
的链接器单元目标(代码库中的大多数目标)自动获得了正确的链接器标志集,以便在需要时拉入Windows shim层。
源代码
此目录仅包含分配器(即shim)层,该层在不同的底层内存分配实现之间切换。
统一的分配器shim
在大多数平台上,Chrome覆盖了malloc
/operator new
符号(以及相应的free
/delete
和其他变体)。这是为了强制执行安全检查,并最近启用了memory-infra堆分析器。
历史上,每个平台在其代码库的不同地方有其特殊的逻辑来定义分配器符号。统一的分配器shim是一个旨在将符号定义和分配器路由逻辑统一到一个中心位置的项目。
完整文档:分配器shim设计文档。
当前状态:在Android、CrOS、Linux、Mac OS和Windows上可用并默认启用。
跟踪问题:crbug.com/550886。
构建时标志:use_allocator_shim
。
统一的分配器shim概述
分配器shim由三个阶段组成:
-
malloc符号定义
这个阶段负责覆盖malloc
、free
、operator new
、operator delete
等符号,并将这些调用路由到分配器shim内部。
这由allocator_shim_override_*
中的头文件处理。在Windows上:Windows的UCRT(通用C运行时)导出了弱符号,我们可以在
allocator_shim_override_ucrt_symbols_win.h
中覆盖它们。在Linux/CrOS上:分配器符号作为导出的全局符号在
allocator_shim_override_libc_symbols.h
(对于malloc
、free
等)和allocator_shim_override_cpp_symbols.h
(对于operator new
、operator delete
等)中定义。
这使malloc
符号在主可执行文件和任何第三方库中得到适当的拦截。在Linux上的符号解析是一个从根链接单元开始的广度优先搜索,即可执行文件(参见便携格式规范:EXECUTABLE AND LINKABLE FORMAT (ELF))。在Android上:与Linux/CrOS的情况不同,加载时的符号拦截是不可能的。这是因为Android进程是从预加载了
libc.so
的Androidzygote通过fork()
创建的,稍后通过dlopen()
加载本机代码(从dlopen()
加载的库中的符号有不同的解析范围)。
在这种情况下,不是通过链接时(即在构建期间)包装符号解析,而是使用--Wl,-wrap,malloc
链接器标志。
使用此包装标志会导致:Chrome代码库中对分配器符号的所有引用都被重写为对
__wrap_malloc
及其同类的引用。__wrap_malloc
符号在allocator_shim_override_linker_wrapped_symbols.h
中定义,并将分配器调用路由到shim层内部。
对原始malloc
符号(通常由系统的libc.so
定义)的引用可通过特殊符号__real_malloc
及其同类访问(将在加载时针对malloc
进行重定位)。
简而言之,这种方法对动态加载器是透明的,它仍然看到对malloc
符号的未定义引用。
这些符号将像往常一样针对libc.so
进行解析。 -
Shim层实现
这个阶段包含了实际的shim实现。这包括:一个单链表的调度器(带有指向类似
malloc
函数的函数指针的结构体)。调度器可以在运行时动态插入(使用InsertAllocatorDispatch
API)。它们可以拦截并覆盖分配器调用。
安全检查(通过std::new_handler
在malloc
失败时自杀等)。
这发生在allocator_shim.cc
中。 -
最终分配器路由
上述调度器链的最终元素在构建时静态定义,并最终将分配器调用路由到实际的分配器(如上文背景部分所述)。这由allocator_shim_default_dispatch_to_*
文件中的头文件处理。