3-6. メモリアロケータ
ベアメタルプログラミングでも開発が進むと動的なコレクションが使いたくなります。std
が使える通常のRustでは、Vec
やString
といった一般的なコレクションが利用できます。このようなコレクションは、ヒープメモリを利用します。そのため、デフォルトでは、no_std
な環境では、これらのコレクションを利用できません。しかし、no_std
なRustでも、メモリアロケータを実装することで、コレクションを利用することができます。
メモリアロケータを実装せずにコレクションを利用する方法は、heaplessで説明します。
ただ、(執筆時点のRust 1.35.0では) 残念なことにnightly必須です。ベアメタルでメモリアロケータを実装するには、allocとalloc_error_handlerのフィーチャが必要です。alloc
は、Rust 1.36でstableになるため、本書が世に出回っている時点では、stableになっています。一方、alloc_error_handler
については、まだ安定化の目途が立っていないようです。今しばらく、メモリアロケータの実装はnightly専用になりそうです。
一時的にツールチェインをnightlyに切り替えます。Cortex-M3を例に解説します。nightlyのツールチェインにCortex-M3用のターゲットを追加します。
$ rustup override set nightly
$ rustup target add thumbv7m-none-eabi
ここでの目標は、次のプログラムを動作させることです。
pub unsafe extern "C" fn Reset() -> ! {
let mut xs = Vec::new();
xs.push(42);
xs.push(83);
println!("{:?}", xs);
}
println!
マクロの実装方法については、print!マクロで説明しています。
グローバルアロケータ
Vec
やString
といったコレクションは、デフォルトではグローバルアロケータを使ってヒープメモリ領域を確保します。グローバルアロケータとは、#[global_allocator]
アトリビュートが指定されたアロケータのことです。このアトリビュートで指定するオブジェクトは、GlobalAllocトレイトを実装しなければなりません。
// グローバルメモリアロケータの宣言
// ユーザはメモリ領域の`[0x2000_0100, 0x2000_0200]`がプログラムの他の部分で使用されないことを
// 保証しなければなりません
#[global_allocator]
static HEAP: BumpPointerAlloc = BumpPointerAlloc {
head: UnsafeCell::new(0x2000_0100),
end: 0x2000_0200,
};
それでは、グローバルアロケータに指定するBumpPointerAlloc
の実装を見てみましょう。
BumpPointerAlloc
これから、BumpPointerAlloc
という最も単純なアロケータを実装します。このアロケータは、次のようにヒープメモリを管理します。
- 初期化時に、ヒープメモリ領域の開始アドレスと終了アドレスを受け取ります
- 割り当て可能なメモリ領域の先頭ポインタを1つだけ保持します
- メモリを新しく割り当てると、割り当てた分だけ単純に先頭ポインタを増加します
- 一度割り当てたメモリは、解放しません
上述した通り、このアロケータは、GlobalAlloc
トレイトを実装します。全体を示します。
use core::ptr;
use core::cell::UnsafeCell;
use core::alloc::GlobalAlloc;
extern crate alloc;
use alloc::alloc::Layout;
use alloc::vec::Vec;
// *シングル*コアシステム用のポインタを増加するだけのアロケータ
struct BumpPointerAlloc {
head: UnsafeCell<usize>,
end: usize,
}
unsafe impl Sync for BumpPointerAlloc {}
unsafe impl GlobalAlloc for BumpPointerAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let head = self.head.get();
let align = layout.align();
let res = *head % align;
let start = if res == 0 { *head } else { *head + align - res };
if start + align > self.end {
// ヌルポインタはメモリ不足の状態を知らせます
ptr::null_mut()
} else {
*head = start + align;
start as *mut u8
}
}
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
// このアロケータはメモリを解放しません
}
}
順番に解説します。
struct BumpPointerAlloc {
head: UnsafeCell<usize>,
end: usize,
}
まず、このアロケータは、割り当て可能なメモリ領域の先頭を示すhead
と、末尾を示すend
を持ちます。head
がUnsafeCell
になっている理由は、&self
を引数に取るalloc
メソッドの中でhead
の値を書き換えるためです。alloc
メソッドのシグネチャは、GlobalAlloc
トレイトで定義されているため、引数を&mut self
に変更することができません。
unsafe impl Sync for BumpPointerAlloc {}
次にSync
トレイトを実装します。これは、グローバルアロケータのオブジェクトがstatic
変数になるため、スレッド間で安全に共有できることをコンパイラに伝えるためです。
GlobalAlloc
トレイトの実装で求められるメソッドは、alloc
とdealloc
のみです。dealloc
はメモリを解放しないため、何もしません。
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let head = self.head.get();
let align = layout.align();
let res = *head % align;
let start = if res == 0 { *head } else { *head + align - res };
if start + align > self.end {
// ヌルポインタはメモリ不足の状態を知らせます
ptr::null_mut()
} else {
*head = start + align;
start as *mut u8
}
}
引数layout
(Layout
) は、要求されているメモリブロックです。align()
で、アライメントを考慮して、確保しなければならないメモリブロックサイズを返します。head
のアドレスがend
に到達するまで、単純にポインタを増加しながら、メモリを割り当てます。
なお、この実装は、割り込みでメモリアロケータを使用する場合、データ競合が発生し、安全に利用できません。
alloc_error_handler
最後の要素が、アロケーションエラー発生時のハンドラです。これは、#[alloc_error_handler]
アトリビュートを指定します。
#[alloc_error_handler]
fn on_oom(_layout: Layout) -> ! {
loop {}
}
今回は、単純に無限ループに陥るだけの実装です。
動作確認
03-bare-metal/allocator
ディレクトリに、Cortex-M3をターゲットにした場合のサンプルコードがあります。ディレクトリに移動し、次のコマンドで実行結果が確認できます。
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.15s
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/allocator`
[42, 83]
無事、Vec
のデバッグ表示が確認できました。
メモリアロケータ実装例
ここで紹介したBumpPointerAlloc
は実用に耐えないものです。いくつか、より洗練されたメモリアロケータの実装例を紹介します。
linked-list-allocator
linked-list-allocatorは、BlogOSの著者が公開しているlinked-listを使ったアロケータです。Writing an OS in Rust (First Edition) Kernel Heapに少し解説があります。
Redox Slab allocator
Redox Slab allocatorは、RustでOSを作るプロジェクト「Redox」のメモリアロケータです。僭越ながら、簡単な解説をRedox Slab Allocatorで学ぶRustベアメタル環境のヒープアロケータに書いています。
alloc-cortex-m
alloc-cortex-mは、linked-list-allocatorを、Cortex-MのMutexを使ってラッピングしたメモリアロケータです。
kernel-roulette
kernel-rouletteは、RustでLinux kernelのdriverを書くプロジェクトです。このプロジェクトでは、kmalloc
やkfree
をFFIで呼び出し、Linux kernelの機能を用いてRustのメモリアロケータを実装します。
出典
- The Embedded Rust Book: コレクション