运行时页与堆分配器
memory/ax-alloc 是运行期物理页、内核字节分配和 Rust GlobalAlloc 的公共入口。它在当前配置下使用 buddy-slab-allocator:Buddy 管理多个物理内存区段,每 CPU Slab 服务小对象,显式页 API 通过页数、对齐、用途和 Normal/DMA32 两条分配函数表达约束。
1. 初始化与内存布局
运行时 allocator 只能接收已经从固件 RAM 中扣除 KImage、reserved、MMIO 和 early allocations 的 Free 区间。所有区间在交接后由 allocator 独占,调用方不得继续直接使用其中的字节。
运行时实现分为公共 API、接线层和算法层。ax-alloc 保存用途、统计与资源所有者;buddy-slab-allocator 只实现 free-list、page metadata 和 size class,不向操作系统暴露第二套分配 API。
下面的源码表可直接用于沿调用链定位问题。公共接口改动集中在 ax-alloc,碎片和锁竞争问题集中在底层算法,平台缺页或 Free 总量错误应回到启动交接检查。
| 源码 | 关键实现 | 主要不变量 |
|---|---|---|
memory/ax-alloc/src/lib.rs | AllocatorOps、UsageKind、Usages、AllocError、page reclaim callback | 一个公共入口、一个统计事实源 |
memory/ax-alloc/src/page.rs | GlobalPage、连续页 slice、Drop | owner 保存页数和用途,恰好释放一次 |
memory/ax-alloc/src/buddy_slab.rs | GlobalAlloc、page adapter、每 CPU Slab | byte 路径固定 CPU;page NoMemory 可在锁外触发注册的 reclaim 回调 |
memory/ax-alloc/src/tracking.rs | 可选 allocation tracking | 仅 tracking feature;不改变生产所有权 |
memory/buddy-slab-allocator/src/global.rs | 小对象/大对象路由、Buddy 锁、section 添加 | Buddy 是唯一页源 |
memory/buddy-slab-allocator/src/buddy/mod.rs | order、split/merge、lowmem 筛选 | 一个 allocation 完全位于一个 section |
memory/buddy-slab-allocator/src/buddy/page_meta.rs | 12 字节 PageMeta 和 flags | Free/Allocated/Slab 状态互斥 |
memory/buddy-slab-allocator/src/slab/ | 固定 size class、partial/full/empty list、remote free | backing 页归 owner CPU 管理 |
表中的边界也决定修改位置:公共用途与回收策略属于 ax-alloc,section 布局、碎片和 remote-free 算法属于 buddy-slab-allocator,平台 Free 区段错误则应回溯到启动交接。
1.1 多区段初始化
os/arceos/modules/axruntime/src/lib.rs::init_allocator() 找到最大的 free region 调用 ax_alloc::global_init(),其余 free region 逐个调用 global_add_memory()。两个入口最终分别调用 buddy_slab_allocator::GlobalAllocator::init() 和 add_region()。
| 入口 | 使用场景 | 失败条件 |
|---|---|---|
global_init(start_vaddr, size) | 建立第一个 Buddy section | 已初始化、范围溢出、metadata/layout 无效 |
global_add_memory(start_vaddr, size) | 增加后续不连续 section | 未初始化、重叠、范围溢出 |
init_percpu_slab(cpu_id) | CPU bring-up 时初始化本地 Slab | CPU id 超过 u16 或重复初始化(两者直接 panic) |
add_region() 对不足以容纳 metadata 和 2 MiB heap 对齐的短 region 会记录日志并跳过。输入 free bytes 与实际 managed_bytes() 的差值来自对齐、metadata 和被跳过的短 region。
1.2 区段元数据
每个 region 的前缀存放 BuddySection 和与页数相关的 PageMeta[],随后将可管理 heap 起点按 REGION_GRANULE = 2 MiB 对齐。metadata 和对齐 padding 不再作为可分配页返回。
多个 region 形成多个独立 section。Buddy 可以在分配时扫描 section,但一个连续 allocation 不会跨越 section 或物理 hole。
每个 section 内部把 metadata 放在被管理内存的前缀,不需要从另一个 allocator 申请描述对象。BuddySection::compute_region_layout_with_heap_align() 用二分搜索计算可容纳的最大页数,保证 BuddySection + PageMeta[] + alignment padding + managed pages 不超过原始 region。
platform Free region
| BuddySection | PageMeta[managed_pages] | padding to 2 MiB | managed page frames |
^ region.start region.end ^
free list 保存物理页帧号的链,PageMeta 保存状态、order 和链表索引。分配时从目标 order 向上寻找 block 并逐级拆分;释放时根据记录的 order 查找 buddy,只有 buddy 同为 Free 且 order 相同才合并。
2. 字节分配
普通 Rust 容器和内核对象通过 GlobalAlloc 进入 ax_alloc::GlobalAllocator::alloc(Layout)。实现依据 size 和 alignment 选择 Slab 或 Buddy,不暴露可切换的 allocator backend feature。
2.1 小对象热路径
满足 size <= 2048 且 align <= 2048 的 allocation 进入 per-CPU Slab。SizeClass 使用固定九档,避免运行期生成动态 class 或复杂 size tree。
| Size class | 对象大小 | Slab backing 规模 |
|---|---|---|
Bytes8 至 Bytes256 | 8、16、32、64、128、256 B | 每个新 Slab 1 页 |
Bytes512、Bytes1024 | 512 B、1024 B | 每个新 Slab 2 页 |
Bytes2048 | 2048 B | 当前公式最多 4 页 |
当本 CPU 对应 class 没有对象时,Slab 返回 NeedsSlab,全局实现从 Buddy 申请 backing pages、标记 PageFlags::Slab,再交给本 CPU class。空 Slab 可以将 backing pages 返回 Buddy。
slab_pages() 在 memory/buddy-slab-allocator/src/slab/size_class.rs 内用三档分支决定单个 Slab 的 backing 页数:8–256 B 用 1 页,512–1024 B 用 2 页,2048 B 最多 4 页。该公式在编译期可推导,避免运行期动态决定 backing 大小。
pub const fn slab_pages(self, page_size: usize) -> usize {
let obj_size = self.size();
if obj_size <= 256 {
1
} else if obj_size <= 1024 {
2
} else {
// 2048-byte objects: 4 pages → header + room for objects
let v = 16 * page_size / (obj_size * 8);
let v = if v < 4 { v } else { 4 };
if v < 1 { 1 } else { v }
}
}
SizeClass::from_layout() 取 size.max(align) 选择最小可容纳 class;若结果超过 2048 B 上限,byte allocation 路径直接退化为 Buddy 大对象。该选择发生在持锁临界区之外,由调用方提前判断。
2.2 大对象与跨 CPU释放
超过 Slab 上限的 byte allocation 被向上取整为 4 KiB 页数,由 Buddy 直接完成。它仍以请求的 Layout 通过 GlobalAlloc 对称释放,不应和显式 页 API 混用。
RustHeap 记账是无条件的:本 CPU 命中、Slab 扩容、跨 CPU 释放和大对象路径都经 SpinLock<Usages> 计入该 bucket,没有独立 feature 开关。跨 CPU 释放只把对象发布给原 owner CPU,不直接操作 Buddy;锁类型、禁止抢占范围、remote-free 原子顺序和锁顺序统一在内存管理锁与并发维护。
3. 显式页接口
页表、用户虚拟内存、页缓存、DMA 和其他需要页粒度所有权的代码直接调用 global_allocator().alloc_pages(num_pages, align, UsageKind) 或 alloc_dma32_pages(num_pages, align, UsageKind)。UsageKind 只表达统计用途;低地址约束由单独的 DMA32 入口表达。
3.1 页请求模型
请求包含连续页数和字节对齐。当前 base page 固定为 4 KiB,具体参数合法性由 buddy-slab-allocator 检查。Buddy 通过平台 virt_to_phys 转换计算普通页和 DMA32 页的候选位置,不能用内核虚拟地址对齐代替物理对齐;这允许直接映射偏移不满足大页对齐时仍返回正确的物理页帧。
fn alloc_pages(&self, num_pages: usize, align: usize, kind: UsageKind) -> AllocResult<usize>;
fn alloc_dma32_pages(&self, num_pages: usize, align: usize, kind: UsageKind) -> AllocResult<usize>;
DMA32 入口只表达物理可达性,UsageKind 只表达用途统计。两者不得组合成大量 page class。当前 page 分配失败时会在 allocator 锁外调用已注册的 page reclaim callback,最多 4 轮;没有注册 callback 或回收 0 页时返回 NoMemory。