3-6. メモリアロケータ

ベアメタルプログラミングでも開発が進むと動的なコレクションが使いたくなります。stdが使える通常のRustでは、VecStringといった一般的なコレクションが利用できます。このようなコレクションは、ヒープメモリを利用します。そのため、デフォルトでは、no_stdな環境では、これらのコレクションを利用できません。しかし、no_stdなRustでも、メモリアロケータを実装することで、コレクションを利用することができます。

メモリアロケータを実装せずにコレクションを利用する方法は、heaplessで説明します。

ただ、(執筆時点のRust 1.35.0では) 残念なことにnightly必須です。ベアメタルでメモリアロケータを実装するには、allocalloc_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!マクロで説明しています。

グローバルアロケータ

VecStringといったコレクションは、デフォルトではグローバルアロケータを使ってヒープメモリ領域を確保します。グローバルアロケータとは、#[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を持ちます。headUnsafeCellになっている理由は、&selfを引数に取るallocメソッドの中でheadの値を書き換えるためです。allocメソッドのシグネチャは、GlobalAllocトレイトで定義されているため、引数を&mut selfに変更することができません。

unsafe impl Sync for BumpPointerAlloc {}

次にSyncトレイトを実装します。これは、グローバルアロケータのオブジェクトがstatic変数になるため、スレッド間で安全に共有できることをコンパイラに伝えるためです。

GlobalAllocトレイトの実装で求められるメソッドは、allocdeallocのみです。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を書くプロジェクトです。このプロジェクトでは、kmallockfreeをFFIで呼び出し、Linux kernelの機能を用いてRustのメモリアロケータを実装します。

出典