裸机 Rust 中的 buddy_system_allocator:用 LockedHeap 提供全局堆,用 FrameAllocator 分配 MMIO 地址空间
2026/9/11 10:43:22 网站建设 项目流程

裸机 Rust 中的 buddy_system_allocator:用 LockedHeap 提供全局堆,用 FrameAllocator 分配 MMIO 地址空间

【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust

buddy_system_allocator是 Rust 生态中一个经典且轻量的伙伴系统(buddy system)内存分配器 crate,在本仓库的裸机(bare-metal)课程中用于解决两类核心问题:一类是通过LockedHeap实现GlobalAlloc,让no_std环境也能安全地使用标准alloccrate 提供的VecString等堆上类型;另一类是通过FrameAllocator分配普通堆以外的地址空间,例如为 PCI BAR 预留 MMIO 区域。读完本文,你将掌握这两个组件的完整用法、仓库内的可运行示例以及配套的构建与运行方式,并理解伙伴系统分配器在无操作系统环境下的适用边界。

为什么裸机环境需要一个分配器

#![no_std]的裸机程序中,没有操作系统提供堆管理,alloccrate 中的VecBoxString等类型都无法直接使用。要启用它们,必须先实现一个全局堆分配器(global heap allocator),也就是实现GlobalAlloctrait,并用#[global_allocator]属性把它注册到二进制文件中。

实现方式有很多:可以自己写、接入现有的分配器,也可以直接使用社区成熟的 crate。本仓库课程(见 alloc 章节)推荐的就是buddy_system_allocator——一个实现基本伙伴系统分配器的 crate。它的价值体现在两个方面:

  • 作为全局堆分配器:通过LockedHeap实现GlobalAlloc,让标准alloccrate 正常工作;
  • 作为通用地址空间分配器:通过FrameAllocator分配任意地址空间,例如为 PCI BAR 划分 MMIO 区域。

从仓库源码看,课程在src/bare-metal/alloc-example/Cargo.tomlsrc/bare-metal/useful-crates/allocator-example/Cargo.toml两处都引入了buddy_system_allocator = "0.13.0",即课程实际使用的版本为 0.13.x,下文示例均以该版本为准。

方式一:用 LockedHeap 实现全局堆分配器

先看全局堆的场景。课程在 alloc-example 示例 中给出了一个完整、可直接编译的no_std堆分配器实现:

#![no_main] #![no_std] extern crate alloc; extern crate panic_halt as _; use alloc::string::ToString; use alloc::vec::Vec; use buddy_system_allocator::LockedHeap; #[global_allocator] static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::<32>::new(); const HEAP_SIZE: usize = 65536; static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE]; pub fn entry() { // SAFETY: `HEAP` is only used here and `entry` is only called once. unsafe { // Give the allocator some memory to allocate. HEAP_ALLOCATOR.lock().init(&raw mut HEAP as usize, HEAP_SIZE); } // Now we can do things that require heap allocation. let mut v = Vec::new(); v.push("A string".to_string()); }

这段代码拆解开来包含四个关键步骤:

  1. 声明分配器并注册为全局分配器static HEAP_ALLOCATOR: LockedHeap<32> = LockedHeap::<32>::new();创建了伙伴系统分配器实例,#[global_allocator]属性让它成为整个二进制唯一的堆分配器。
  2. 预留静态堆内存static mut HEAP: [u8; 65536]在数据段中预留 64 KiB 的堆空间。伙伴系统分配器本身不拥有内存,它需要调用方在初始化时把一段连续内存“交”给它。
  3. 初始化:在entry()中通过HEAP_ALLOCATOR.lock().init(heap_start, heap_size)HEAP的起始地址与大小交给分配器。注意init需要unsafe,因为HEAPstatic mut,且课程源码注释明确约定“HEAP只在这里使用、entry只被调用一次”。
  4. 之后即可正常使用堆类型Vec::new()"A string".to_string()等操作会透明地走LockedHeap分配与释放。

理解 LockedHeap 的 const 参数:最大 order

LockedHeap<32>中的<32>是编译期常量参数,表示分配器的最大 order(阶),即伙伴系统允许合并的最大内存块阶数。课程在 alloc.md 的细节说明中给出了量化结论:

The const parameter ofLockedHeapis the max order of the allocator; i.e. in this case it can allocate regions of up to 2**32 bytes.

也就是说,LockedHeap<32>单次最多可以分配 2³² 字节(4 GiB)的区域。实际的伙伴系统逻辑是:分配请求向上取整到 2 的幂次大小,在对应的阶上查找空闲块,若没有则从更高阶块逐级拆分;释放时则把相邻的空闲伙伴块逐级合并回去。max order决定了这套“拆分-合并”机制的上限,也决定了内部空闲链表数组的长度,因此需要根据目标平台的内存规模合理选择。

使用 LockedHeap 时的注意事项

  • 全局分配器必须唯一:如果依赖树中任何一个 crate 依赖了alloc,那么二进制文件中就必须且只能有一个全局分配器,通常定义在最顶层的 binary crate 中。若重复定义,链接阶段会直接报错。
  • panic handler 必须存在extern crate panic_halt as _;的作用是强制链接panic_haltcrate,为no_std程序提供 panic handler,否则链接器会因缺少 panic 处理符号而失败。
  • 构建与运行前提:课程明确说明这个示例“可以编译但无法运行”,因为它没有真正的入口点(entry只是普通函数,没有调用它的启动代码);要实际跑起来,需要在真实硬件或模拟器(如 QEMU)中配合启动代码使用。
  • 其他替代方案:伙伴系统分配器不是唯一选择,你也可以编写自己的分配器或接入已有的分配器;buddy_system_allocator的价值在于开箱即用且代码量小,适合课程演示与教学。

方式二:用 FrameAllocator 分配堆之外的地址空间

buddy_system_allocator的第二类用途是不把它当作GlobalAlloc,而是当作一个通用的地址空间分配器。这正是本仓库 useful-crates 章节 的核心示例场景:为 PCI BAR 分配 MMIO 空间。

PCI 设备通过基地址寄存器(Base Address Register,BAR)向系统声明其需要映射的 MMIO 区域。驱动在初始化时需要为每个 BAR 找到一个尚未被占用的、大小和对齐都满足要求的地址区间。这类需求并不适合走普通堆,而更适合由专门的地址空间分配器来管理。课程给出的示例(完整源码见 allocator-example/src/main.rs)如下:

use buddy_system_allocator::FrameAllocator; use core::alloc::Layout; fn main() { let mut allocator = FrameAllocator::<32>::new(); allocator.add_frame(0x200_0000, 0x400_0000); let layout = Layout::from_size_align(0x100, 0x100).unwrap(); let bar = allocator .alloc_aligned(layout) .expect("Failed to allocate 0x100 byte MMIO region"); println!("Allocated 0x100 byte MMIO region at {:#x}", bar); }

逐行拆解 FrameAllocator 示例

  1. 创建分配器FrameAllocator::<32>::new()创建一个初始不持有任何内存的伙伴系统分配器。与LockedHeap一样,<32>同样是最大 order,决定可管理地址空间的上界。
  2. 登记地址区间allocator.add_frame(0x200_0000, 0x400_0000)0x200_00000x400_0000这段地址空间加入空闲列表(区间为左闭右开:[0x200_0000, 0x400_0000),共 32 MiB)。这一步等价于告诉分配器“这些地址是空闲可用的”。
  3. 构造分配布局Layout::from_size_align(0x100, 0x100)描述一次分配请求:大小为 0x100 字节、对齐要求也是 0x100 字节。Layout类型来自core::alloc,与GlobalAlloc::alloc的请求描述方式完全一致。
  4. 分配并对齐allocator.alloc_aligned(layout)从登记的空闲区间中切出一块满足大小与对齐要求的子区间,返回起始地址(示例中是usize)。失败时返回错误,示例用expect直接断言成功。
  5. 输出结果println!("Allocated 0x100 byte MMIO region at {:#x}", bar)以十六进制打印分配到的地址,方便在真实硬件上核对 BAR 映射值。

为什么这里用“对齐等于大小”

课程在该示例的说明中特别强调了一个硬件事实:

PCI BARs always have alignment equal to their size.

PCI 规范要求 BAR 的地址对齐等于其大小:一个 0x100 字节的 BAR 必须 0x100 对齐,一个 1 MiB 的 BAR 必须 1 MiB 对齐。正因为如此,示例中构造Layout时把sizealign都设为 0x100 是符合 PCI 语义的。而伙伴系统分配器的天然特性恰好满足这一要求——它分配的块总是 2 的幂大小且按同阶对齐,因此用于 BAR 分配非常契合。如果硬件的对齐规则更复杂,alloc_aligned也能接受任意的Layout,只需按设备手册正确构造即可。

运行方式与限制

课程给出了明确的运行指引:

  • src/bare-metal/useful-crates/allocator-example/目录下执行cargo run即可运行该示例;
  • 不能在 Rust Playground 中运行,因为示例依赖buddy_system_allocator这个外部 crate,Playground 无法解析该依赖。

此外,仓库还为该示例提供了 Bazel 构建配置(见 BUILD.bazel):通过rust_binary构建allocator-example可执行文件,依赖由all_crate_deps(normal = True)从 Bazel 管理的 crate 仓库统一解析;同时定义了一个rust_testallocator-example_test)验证构建产物。如果你在仓库环境中使用 Bazel 而非 Cargo,可以直接复用这套配置。

两个组件的关系与选型总结

LockedHeapFrameAllocator是同一个伙伴系统核心的两种对外形态,可用下表对比:

维度LockedHeapFrameAllocator
主要用途实现GlobalAlloc,支撑alloccrate分配任意地址空间(如 MMIO、帧)
使用前提#[global_allocator]注册,需先init堆内存add_frame登记空闲地址区间
是否加锁内部自带锁(lock()访问)需要调用方自行保证独占(示例中为局部变量)
典型场景no_std程序的Vec/String/BoxPCI BAR 的 MMIO 地址分配
版本依赖buddy_system_allocator = "0.13.0"(课程仓库实际版本)同左

选型时遵循一条主线:只要你想让堆分配类型可用,就走LockedHeap+#[global_allocator];只要你想管理堆之外的地址资源,就走FrameAllocator+add_frame+alloc_aligned两者共享相同的伙伴系统拆分/合并算法,因此内存使用模式(2 的幂块、伙伴合并)也一致。

在课程中的定位与延伸阅读

本文讨论的buddy_system_allocator是裸机课程“实用 crate”专题的一部分,该专题还介绍了页表管理 crateaarch64-paging(见 aarch64-paging 章节),两者结合起来可以覆盖裸机系统中“内存堆管理 + 虚拟地址空间管理”两条主线。相关章节在课程目录(SUMMARY.md)中归属于bare-metal模块,属于学员掌握no_std开发后的进阶内容。

如果你需要完整的裸机分配方案,可以进一步阅读:

  • alloc 章节:全局堆分配器的原理与使用约束(唯一性、panic handler、max order 语义);
  • alloc-example 完整源码:LockedHeap全局分配器的完整no_std实现;
  • allocator-example 完整源码:FrameAllocator分配 MMIO 空间的完整示例;
  • allocator-example 的 Cargo.toml 与 Bazel 配置:两种构建方式的实际依赖声明。

掌握这两个入口之后,你便可以在自己的裸机项目中自由地组合使用伙伴系统分配器,无论是为内核堆供能,还是为设备驱动预留地址空间。

【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询