diff --git a/blog/content/edition-2/posts/10-heap-allocation/index.ja.md b/blog/content/edition-2/posts/10-heap-allocation/index.ja.md index cd72a2f2..7f55f2b6 100644 --- a/blog/content/edition-2/posts/10-heap-allocation/index.ja.md +++ b/blog/content/edition-2/posts/10-heap-allocation/index.ja.md @@ -10,7 +10,7 @@ translation_based_on_commit = "" translators = ["woodyZootopia"] +++ -この記事では、私たちのカーネルにヒープ割り当ての機能を追加します。まず動的メモリの基礎を説明し、いかにして借用チェッカがありがちなアロケーションエラーを防ぐのかを示します。その後Rustの基礎的なアロケーションインターフェースを実装し、ヒープメモリ領域を作成し、アロケータクレートを作成します。この記事を終える頃には、Rustに組み込みの`alloc`クレートのすべてのアロケーション・コレクション型が私たちのカーネルで利用可能になっているでしょう。 +この記事では、私たちのカーネルにヒープ割り当て (アロケーション) の機能を追加します。まず動的メモリの基礎を説明し、どのようにして借用チェッカがありがちなアロケーションエラーを防いでくれるのかを示します。その後Rustの基本的なアロケーションインターフェースを実装し、ヒープメモリ領域を作成し、アロケータクレートを設定します。この記事を終える頃には、Rustに組み込みの`alloc`クレートのすべてのアロケーション・コレクション型が私たちのカーネルで利用可能になっているでしょう。 @@ -28,24 +28,24 @@ translators = ["woodyZootopia"] ## ローカル (局所) 変数とスタティック (静的) 変数 -私たちのカーネルでは現在二種類の変数が使用されています:ローカル変数と`static`変数です。ローカル変数は[コールスタック][call stack]に格納されており、変数の定義された関数がリターンするまでの間のみ有効です。スタティック変数はメモリ上の固定された場所に格納されており、プログラムのライフタイム全体で常に生存します。 +私たちのカーネルでは現在二種類の変数が使用されています:ローカル変数と`static`変数です。ローカル変数は[コールスタック][call stack]に格納されており、変数の定義された関数がリターンするまでの間のみ有効です。スタティック変数はメモリ上の固定された場所に格納されており、プログラムのライフタイム全体で常に生存しています。 ### ローカル変数 -ローカル変数は[コールスタック][call stack]に格納されています。これは`push`と`pop`という命令をサポートする[スタックというデータ構造][stack data structure]です。関数に入るたびに、パラメータ、リターンアドレス、呼び出された関数のローカル変数がコンパイラによってpushされます: +ローカル変数は[コールスタック][call stack]に格納されています。これは`プッシュ`と`ポップ`という命令をサポートする[スタックというデータ構造][stack data structure]です。関数に入るたびに、パラメータ、リターンアドレス、呼び出された関数のローカル変数がコンパイラによってプッシュされます: [call stack]: https://en.wikipedia.org/wiki/Call_stack [stack data structure]: https://en.wikipedia.org/wiki/Stack_(abstract_data_type) ![An outer() and an inner(i: usize) function. Both have some local variables. Outer calls inner(1). The call stack contains the following slots: the local variables of outer, then the argument `i = 1`, then the return address, then the local variables of inner.](call-stack.svg) -上の例は、`outer`関数が`inner`関数を呼び出した後のコールスタックを示しています。コールスタックは`outer`のローカル変数を先に持っていることが分かります。`inner`を呼び出すと、パラメータ`1`とこの関数のリターンアドレスがpushされます。そこで制御は`inner`へと移り、`inner`は自身のローカル変数をpushします。 +上の例は、`outer`関数が`inner`関数を呼び出した後のコールスタックを示しています。コールスタックは`outer`のローカル変数を先に持っていることが分かります。`inner`を呼び出すと、パラメータ`1`とこの関数のリターンアドレスがプッシュされます。そこで制御は`inner`へと移り、`inner`は自身のローカル変数をプッシュします。 -`inner`関数がリターンした後で、コールスタックのその関数の部分がpopされ、`outer`のローカル変数のみが残ります: +`inner`関数がリターンすると、コールスタックのこの関数に対応する部分がポップされ、`outer`のローカル変数のみが残ります: ![The call stack containing only the local variables of outer](call-stack-return.svg) -`inner`関数のローカル変数はリターンまでしか生存していないことが分かります。Rustコンパイラはこの生存期間 (ライフタイム) を強制し、私たちが値を長く使いすぎてしまうとエラーを投げます。例えば、ローカル変数への参照を返そうとすると: +`inner`関数のローカル変数はリターンまでしか生存していないことが分かります。Rustコンパイラはこの生存期間 (ライフタイム) を強制し、私たちが値を長く使いすぎてしまうとエラーを投げます。例えば、ローカル変数への参照を返そうとしたときがそうです: ```rust fn inner(i: usize) -> &'static u32 { @@ -54,26 +54,26 @@ fn inner(i: usize) -> &'static u32 { } ``` -([run the example on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6186a0f3a54f468e1de8894996d12819)) +([この例をplaygroundで実行する](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6186a0f3a54f468e1de8894996d12819)) -この例で参照を返そうとすることには意味がありませんが、変数に関数よりも長く生存して欲しいというケースは存在します。すでに私たちのカーネルでそのようなケースに遭遇しています。[割り込み記述子表をロード][load an interrupt descriptor table]しようとしたときで、ライフタイムを延ばすために`static`変数を使う必要があったのでした。 +上の例の場合、参照を返すことには意味がありませんが、変数に関数よりも長く生存して欲しいというケースは存在します。すでに私たちのカーネルでそのようなケースに遭遇しています:それは[割り込み記述子表 (IDT) を読み込][load an interrupt descriptor table]もうとしたときで、ライフタイムを延ばすために`static`変数を使う必要があったのでした。 -[load an interrupt descriptor table]: @/edition-2/posts/05-cpu-exceptions/index.md#loading-the-idt +[load an interrupt descriptor table]: @/edition-2/posts/05-cpu-exceptions/index.ja.md#idtwodu-miip-mu ### スタティック変数 -スタティック変数は、スタックとは別の固定されたメモリ位置に格納されます。このメモリ位置はコンパイル時にリンカによって指定され、実行可能ファイルにエンコードされています。スタティック変数はプログラムの実行中ずっと生存するため、`'static`ライフタイムを持っており、ローカル変数によっていつでも参照されることができます。 +スタティック変数は、スタックとは別の固定されたメモリ位置に格納されます。このメモリ位置はコンパイル時にリンカによって指定され、実行可能ファイルにエンコードされています。スタティック変数はプログラムの実行中ずっと生存するため、`'static`ライフタイムを持っており、ローカル変数によっていつでも参照することができます。 ![The same outer/inner example with the difference that inner has a `static Z: [u32; 3] = [1,2,3];` and returns a `&Z[i]` reference](call-stack-static.svg) -上の例で`inner`関数がリターンするとき、それに対応するコールスタックは破棄されます。スタティック変数は絶対に破棄されない別のメモリ領域にあるため、参照`&Z[1]`はリターン後も有効です。 +上の例で`inner`関数がリターンするとき、それに対応するコールスタックは破棄されます。(しかし)スタティック変数は絶対に破棄されない別のメモリ領域にあるため、参照`&Z[1]`はリターン後も有効です。 -`'static`ライフタイムの他にもスタティック変数には利点があります:位置がコンパイル時に分かるため、アクセスするために参照が必要ないのです。この特性を私たちの`println`マクロに利用しました:[スタティックな`Writer`][static `Writer`]を内部で使うことで、マクロを呼び出す際に`&mut Writer`が必要でなくなるのですが、これは他の変数にアクセスできない[例外処理][exception handlers]においてとても便利なのです。 +`'static`ライフタイムの他にもスタティック変数には利点があります:位置がコンパイル時に分かるため、アクセスするために参照が必要ないのです。この特性を私たちの`println`マクロを作る際に利用しました:[スタティックな`Writer`][static `Writer`]をその内部で使うことで、マクロを呼び出す際に`&mut Writer`参照が必要でなくなります。これは他の変数にアクセスできない[例外処理関数][exception handlers]においてとても有用です。 [static `Writer`]: @/edition-2/posts/03-vga-text-buffer/index.md#a-global-interface [exception handlers]: @/edition-2/posts/05-cpu-exceptions/index.md#implementation -しかし、スタティック変数のこの特性には重大な欠点がついてきます:デフォルトでは読み込み専用なのです。Rustがこのルールを強制するのは、例えば二つのスレッドがあるスタティック変数を同時に変更した場合[データ競合][data race]が発生するためです。スタティック変数を変更する唯一の方法は、それを[`Mutex`]型にカプセル化し、あらゆる時刻において`&mut`参照が一つしか存在しないことを保証することです。`Mutex`はすでに[スタティックなVGAバッファへの`Writer`][vga mutex]を作ったときに使いました。 +しかし、スタティック変数のこの特性には重大な欠点がついてきます:デフォルトでは読み込み専用なのです。Rustがこのルールを強制するのは、例えば二つのスレッドがあるスタティック変数を同時に変更した場合[データ競合][data race]が発生するためです。スタティック変数を変更する唯一の方法は、それを[`Mutex`]型にカプセル化し、あらゆる時刻において`&mut`参照が一つしか存在しないことを保証することです。`Mutex`はすでに[VGAバッファへのスタティックな`Writer`][vga mutex]を作ったときに使いました。 [data race]: https://doc.rust-lang.org/nomicon/races.html [`Mutex`]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html @@ -81,7 +81,7 @@ fn inner(i: usize) -> &'static u32 { ## 動的 (ダイナミック) メモリ -ローカル変数とスタティック変数を組み合わせれば、それら自体とても強力であり、殆どのユースケースを満足します。しかし、両方に制限が存在することも見てきました: +ローカル変数とスタティック変数を組み合わせれば、それら自体とても強力であり、殆どのユースケースを満足します。しかし、どちらにも制限が存在することも見てきました: - ローカル変数はそれを定義する関数やブロックが終わるまでしか生存しません。なぜなら、これらはコールスタックに存在し、関数がリターンした段階で破棄されるからです。 - スタティック変数はプログラムの実行中常に生存するため、必要なくなったときでもメモリを取り戻したり再利用したりする方法がありません。また、所有権のセマンティクスが不明瞭であり、すべての関数からアクセスできてしまうため、変更しようと思ったときには[`Mutex`]で保護してやらないといけません。 @@ -90,46 +90,46 @@ fn inner(i: usize) -> &'static u32 { [unsized rvalues]: https://github.com/rust-lang/rust/issues/48055 -これらの欠点を回避するために、プログラミング言語はしばしば、変数を格納するための第三の領域である**ヒープ**をサポートします。ヒープは、`allocate`と`deallocate`という二つの関数を通じて、実行時の**動的メモリ割り当て**をサポートします。以下のように:`allocate`関数は、変数を格納するのに使える、指定されたサイズの解放されたメモリの塊を返します。この変数は、`deallocate`関数をその変数への参照を引数に呼び出すことによって解放されるまで生存します。 +これらの欠点を回避するために、プログラミング言語はしばしば、変数を格納するための第三の領域である**ヒープ**をサポートします。ヒープは、`allocate`と`deallocate`という二つの関数を通じて、実行時の**動的メモリ割り当て**をサポートします。仕組みとしては以下のようになります:`allocate`関数は、変数を格納するのに使える、指定されたサイズの解放されたメモリの塊を返します。変数への参照を引数に`deallocate`関数を呼び出すことによってその変数を解放するまで、この変数は生存します。 例を使って見てみましょう: ![The inner function calls `allocate(size_of([u32; 3]))`, writes `z.write([1,2,3]);`, and returns `(z as *mut u32).offset(i)`. The outer function does `deallocate(y, size_of(u32))` on the returned value `y`.](call-stack-heap.svg) -ここで`inner`関数は`z`を格納するためにスタティック変数ではなくヒープメモリを使っています。まず要求されたサイズのメモリブロックを割り当て、`*mut u32`の[生ポインタ][raw pointer]を返されます。その後で[`ptr::write`]メソッドを使って配列`[1,2,3]`をこれに書き込みます。最後のステップとして、[`offset`]関数を使って`i`番目の要素へのポインタを計算しそれを返します(簡単のため、必要なキャストやunsafeブロックをいくつか省略しました)。 +ここで`inner`関数は`z`を格納するためにスタティック変数ではなくヒープメモリを使っています。まず要求されたサイズのメモリブロックを割り当て、`*mut u32`の[生ポインタ][raw pointer]を受け取ります。その後で[`ptr::write`]メソッドを使ってこれに配列`[1,2,3]`を書き込みます。最後のステップとして、[`offset`]関数を使って`i`番目の要素へのポインタを計算しそれを返します(簡単のため、必要なキャストやunsafeブロックをいくつか省略しました)。 [raw pointer]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer [`ptr::write`]: https://doc.rust-lang.org/core/ptr/fn.write.html [`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset -割り当てられたメモリは`deallocate`の呼び出しによって明示的に解放されるまで生存します。したがって、返されたポインタは、`inner`がリターンしコールスタックの対応する部分が破棄された後も有効です。スタティックメモリと比較したときのヒープメモリの長所は、解放後に再利用できると言うことです(`outer`内の`deallocate`呼び出しでまさにこれを行っています)。この呼び出しの後、状況は以下のようになります。 +割り当てられたメモリは`deallocate`の呼び出しによって明示的に解放されるまで生存します。したがって、返されたポインタは、`inner`がリターンしコールスタックの対応する部分が破棄された後も有効です。スタティックメモリと比較したときのヒープメモリの長所は、解放(`outer`内の`deallocate`呼び出しでまさにこれを行っています)後に再利用できるということです。この呼び出しの後、状況は以下のようになります。 ![The call stack contains the local variables of outer, the heap contains z[0] and z[2], but no longer z[1].](call-stack-heap-freed.svg) -`z[1]`スロットが再び解放され、次の`allocate`呼び出しで再利用できることが分かります。しかし、`z[0]`と`z[2]`は永久にdeallocateされず、したがって永久に解放されないことも分かります。このようなバグは**メモリリーク**と呼ばれており、しばしばプログラムによる過剰なメモリ消費を引き起こします(`inner`をループ内で何度も呼び出したらどんなことになるか、想像してみてください)。これ自体良くないことに思われるかもしれませんが、実は動的割り当てでは遙かに危険性の高いバグも発生しうるのです。 +`z[1]`スロットが解放され、次の`allocate`呼び出しで再利用できることが分かります。しかし、`z[0]`と`z[2]`は永久にdeallocateされず、したがって永久に解放されないことも分かります。このようなバグは**メモリリーク**と呼ばれており、しばしばプログラムの過剰なメモリ消費を引き起こします(`inner`をループで何度も呼び出したらどんなことになるか、想像してみてください)。これ自体良くないことに思われるかもしれませんが、実は動的割り当てでは遙かに危険性の高いバグも発生しうるのです。 -### Common Errors +### よくあるミス メモリリークは困りものですが、プログラムを攻撃者に対して脆弱にはしません。しかしこのほかに、より深刻な結果を招く二種類のバグが存在します: - もし変数に対して`deallocate`を呼んだ後にも間違ってそれを使い続けたら、いわゆるuse-after-free (メモリ解放後に使用) 脆弱性が発生します。このようなバグは未定義動作を引き起こし、しばしば攻撃者が任意コードを実行するのに利用されます。 - 間違ってある変数を二度解放したら、double-free (二重解放) 脆弱性が発生します。これが問題になるのは、最初の`deallocate`呼び出しの後に同じ場所にallocateされた別の割り当てを解放してしまうかもしれないからです。従って、これもまたuse-after-free脆弱性につながりかねません。 -これらの種類の脆弱性は広く知られているため、回避する方法も分かっているはずだとお思いになるかもしれません。しかし答えはいいえで、このような脆弱性は未だ散見され、例えば最近でも任意コード実行を許す[Linuxのuse-after-free脆弱性][linux vulnerability]が存在しました。このことは、最高のプログラマーであっても、複雑なプロジェクトにおいて常に正しく動的メモリを扱えはしないと言うことを示しています。 +これらの脆弱性は広く知られているため、回避する方法も解明されているはずだとお思いになるかもしれません。しかし答えはいいえで、このような脆弱性は未だ散見され、例えば最近でも任意コード実行を許す[Linuxのuse-after-free脆弱性][linux vulnerability]が存在しました。このことは、最高のプログラマーであっても、複雑なプロジェクトにおいて常に正しく動的メモリを扱えはしないということを示しています。 [linux vulnerability]: https://securityboulevard.com/2019/02/linux-use-after-free-vulnerability-found-in-linux-2-6-through-4-20-11/ -これらの問題を回避するため、JavaやPythonといった多くの言語では[**ガベージコレクション**][_garbage collection_]という技法を使って自動的に動的メモリを管理しています。発想としては、プログラマが絶対に自分の手で`deallocate`を呼び出すことがないようにするというものです。代わりに、プログラムが定期的に一時停止されてスキャンされ、未使用のヒープ変数が見つかったら自動的にdeallocateされるのです。従って、上のような脆弱性は絶対に発生し得ません。欠点としては,定期的にスキャンすることによる性能のオーバーヘッドが発生することと、一時停止の時間が長くなりうるということがあります。 +これらの問題を回避するため、JavaやPythonといった多くの言語では[**ガベージコレクション**][_garbage collection_]という技術を使って自動的に動的メモリを管理しています。発想としては、プログラマが絶対に自分の手で`deallocate`を呼び出すことがないようにするというものです。代わりに、プログラムが定期的に一時停止されてスキャンされ、未使用のヒープ変数が見つかったら自動的にdeallocateされるのです。従って、上のような脆弱性は絶対に発生し得ません。欠点としては,定期的にスキャンすることによる性能のオーバーヘッドが発生することと、一時停止の時間が長くなりがちであることが挙げられます。 [_garbage collection_]: https://en.wikipedia.org/wiki/Garbage_collection_(computer_science) -Rustはこの問題に対して別のアプローチを取ります:[**所有権**][_ownership_]と呼ばれる概念を使って、動的メモリの操作の正確性をコンパイル時にチェックするのです。従って前述の脆弱性を回避するためのガベージコレクションの必要がなく、性能のオーバーヘッドが存在しないと言うことです。このアプローチのもう一つの利点として、CやC++と同様、プログラマが動的メモリの使用に関して精緻な制御を行うことができるということが挙げられます。 +Rustはこの問題に対して別のアプローチを取ります:[**所有権**][_ownership_]と呼ばれる概念を使って、動的メモリの操作の正確性をコンパイル時にチェックするのです。従って前述の脆弱性を回避するためのガベージコレクションの必要がなく、性能のオーバーヘッドが存在しません。このアプローチのもう一つの利点として、CやC++と同様、プログラマが動的メモリの使用に関して精緻な制御を行うことができるということが挙げられます。 [_ownership_]: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html ### Rustにおける割り当て -プログラマーに自分の手で`allocate`と`deallocate`を呼ばせる代わりに、Rustの標準ライブラリはこれらの関数を暗黙の内に呼ぶ抽象型を提供しています。最も重要な型は[**`Box`**]で、これはヒープに割り当てられた値の抽象化です。これは[`Box::new`]コンストラクタ関数を提供しており、これは値を引数として、その値のサイズを引数に`allocate`を呼び出し、ヒープ上に新しく割り当てられたスロットにその値を移動 (ムーブ) します。ヒープメモリを再び解放するために、`Box`型は自身がスコープから出た際に`deallocate`を呼べるよう[`Drop`トレイト][`Drop` trait]を実装しています。 +プログラマーに自分の手で`allocate`と`deallocate`を呼ばせる代わりに、Rustの標準ライブラリはこれらの関数を暗黙の内に呼ぶ抽象型を提供しています。最も重要な型は[**`Box`**]で、これはヒープに割り当てられた値の抽象化です。これは[`Box::new`]コンストラクタ関数を提供しており、これは値を引数として、その値のサイズを引数に`allocate`を呼び出し、ヒープ上に新しく割り当てられたスロットにその値を移動 (ムーブ) します。ヒープメモリを解放するために、スコープから出た際に`deallocate`を呼ぶような[`Drop`トレイト][`Drop` trait]を`Box`型は実装しています。 [**`Box`**]: https://doc.rust-lang.org/std/boxed/index.html [`Box::new`]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html#method.new @@ -139,25 +139,25 @@ Rustはこの問題に対して別のアプローチを取ります:[**所有 { let z = Box::new([1,2,3]); […] -} // zがスコープから出て`deallocate`が呼ばれる +} // zがスコープから出たので`deallocate`が呼ばれる ``` -このようなパターンは[リソース確保が初期化である][_resource acquisition is initialization_](resource acquisition is initialization、略してRAII)という奇妙な名前を持っています。このパターンは、C++で[`std::unique_ptr`]という似た抽象型を実装するのに使われたのが起源です。 +このようなパターンは[リソース取得は初期化である][_resource acquisition is initialization_](resource acquisition is initialization、略してRAII)という奇妙な名前を持っています。C++で[`std::unique_ptr`]という同じような抽象型を実装するのに使われたのが始まりです。 [_resource acquisition is initialization_]: https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization [`std::unique_ptr`]: https://en.cppreference.com/w/cpp/memory/unique_ptr -このような型自体はすべてのuse-after-freeバグを防ぐのに十分ではありません。なぜなら、プログラマは、`Box`がスコープ外に出て対応するヒープメモリスロットがdeallocateされた後でも参照を利用し続けることができるからです: +このような型自体ではすべてのuse-after-freeバグを防ぐのに十分ではありません。なぜなら、プログラマは、`Box`がスコープ外に出て対応するヒープメモリスロットがdeallocateされた後でも参照を利用し続けることができてしまうからです: ```rust let x = { let z = Box::new([1,2,3]); &z[1] -}; // zがスコープから出て`deallocate`が呼ばれる +}; // zがスコープから出たので`deallocate`が呼ばれる println!("{}", x); ``` -ここでRustの所有権の出番です。所有権システムは、参照が有効なスコープを表す抽象[ライフタイム][lifetime]をそれぞれの参照に指定します。上の例では、参照`x`は配列`z`から取られているので、`z`ガスコープ外に出ると無効になります。[上の例をplaygroundで実行する][playground-2]と、確かにRustコンパイラがエラーを投げるのが分かります: +ここでRustの所有権の出番です。所有権システムは、参照が有効なスコープを表す抽象[ライフタイム][lifetime]をそれぞれの参照に指定します。上の例では、参照`x`は配列`z`から取られているので、`z`がスコープ外に出ると無効になります。[上の例をplaygroundで実行する][playground-2]と、確かにRustコンパイラがエラーを投げるのが分かります: [lifetime]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html [playground-2]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=28180d8de7b62c6b4a681a7b1f745a48 @@ -175,18 +175,18 @@ error[E0597]: `z[_]` does not live long enough | - `z[_]` dropped here while still borrowed ``` -ここで使われている用語は初見では少しわかりにくいかもしれません。値の参照を取ることは値を借用する (borrow) と呼ばれています。これは現実での借用と似ているためです:オブジェクトへの一時的なアクセスができるようになりますが、それをいつか返さなければならず、また破壊することも許されません。オブジェクトが破壊される前にすべての借用が終了することを確かめることにより、Rustコンパイラはuse-after-freeが起こりえないことを保証できるのです。 +ここで使われている用語は初見では少しわかりにくいかもしれません。値の参照を取ることは値を借用する (borrow) と呼ばれています。これは現実での借用と似ているためです:オブジェクトに一時的にアクセスできるようになりますが、それをいつか返さなければならず、また破壊することも許されません。オブジェクトが破壊される前にすべての借用が終了することを確かめることにより、Rustコンパイラはuse-after-freeが起こりえないことを保証できるのです。 -Rustの所有権システムはさらに突き詰められており、use-after-freeバグを防ぐだけでなく、JavaやPythonのようなガベージコレクション型言語と同じ完全な[メモリ安全性 (セーフティ) ][_memory safety_]を提供しています。さらに[スレッド安全性 (セーフティ) ][_thread safety_]も保証されており、マルチスレッドのプログラムにおいてはこれらの言語よりもさらに安全です。さらに最も重要なことに、これらのチェックは全てコンパイル時に行われるため、C言語で手書きされたメモリ管理と比べても、実行時のオーバーヘッドはありません。 +Rustの所有権システムはさらに突き詰められており、use-after-freeバグを防ぐだけでなく、JavaやPythonのようなガベージコレクション型言語と同じ完全な[メモリ安全性 (セーフティ) ][_memory safety_]を提供しています。さらに[スレッド安全性 (セーフティ) ][_thread safety_]も保証されており、マルチスレッドのプログラムにおいてはこれらの言語よりもさらに安全です。さらに最も重要なことに、これらのチェックは全てコンパイル時に行われるため、C言語で手書きされたメモリ管理と比べても実行時のオーバーヘッドはありません。 [_memory safety_]: https://en.wikipedia.org/wiki/Memory_safety [_thread safety_]: https://en.wikipedia.org/wiki/Thread_safety -### 使い方 +### 使用例 Rustにおける動的メモリ割り当ての基礎を学んだわけですが、これをいつ使えば良いのでしょうか?私たちのカーネルは動的メモリ割り当てなしにこれだけやってこられたのに、どうして今になってこれが必要なのでしょうか? -まず覚えておいて欲しいのは、動的メモリ割り当てには、割り当てを行うたびにヒープから空いているスロットを探してこないといけないので、少しだけ性能オーバーヘッドがあるということです。このため、特に性能が重要となるカーネルのプログラムにおいては、一般にローカル変数の方が好ましいです。しかし、動的メモリ割り当てが最良の選択肢であるようなケースも存在するのです。 +まず覚えておいて欲しいのは、割り当てを行うたびにヒープから空いているスロットを探してこないといけないので、動的メモリ割り当てには少しだけ性能オーバーヘッドがあるということです。このため、特に性能が重要となるカーネルのプログラムにおいては、一般にローカル変数の方が好ましいです。しかし、動的メモリ割り当てが最良の選択肢であるようなケースも存在するのです。 基本的なルールとして、動的メモリは動的なライフタイムや可変サイズを持つような変数に必要とされます。動的なライフタイムを持つ最も重要な型は[**`Rc`**]で、これはラップされた値に対する参照を数えておき、すべての参照がスコープから外れたらそれをdeallocateするというものです。可変サイズを持つ型の例には、[**`Vec`**]、[**`String`**]、その他の[コレクション型][collection types]といった、要素が追加されたときに動的に大きくなるような型が挙げられます。これらの型は、容量が一杯になると、より大きい量のメモリを割り当て、すべての要素をコピーし、古い割り当てをdeallocateすることにより対処します。 @@ -197,9 +197,9 @@ Rustにおける動的メモリ割り当ての基礎を学んだわけですが 私たちのカーネルでは主にコレクション型を必要とし、例えば、将来の記事でマルチタスキングを実行するときにアクティブなタスクのリストを格納するために使います。 -## アロケータのインターフェース +## アロケータインターフェース -ヒープアロケータを実装するための最初のステップは、組み込みの[`alloc`]クレートへの依存関係を津一過することです。[`core`]クレートと同様、これは標準ライブラリのサブセットであり、アロケーション型やコレクション型を含んでいます。`alloc`への依存関係を追加するために、以下を`lib.rs`に追加します: +ヒープアロケータを実装するための最初のステップは、組み込みの[`alloc`]クレートへの依存関係を追加することです。[`core`]クレートと同様、これは標準ライブラリのサブセットであり、アロケーション型やコレクション型を含んでいます。`alloc`への依存関係を追加するために、以下を`lib.rs`に追加します: [`alloc`]: https://doc.rust-lang.org/alloc/ [`core`]: https://doc.rust-lang.org/core/ @@ -210,11 +210,11 @@ Rustにおける動的メモリ割り当ての基礎を学んだわけですが extern crate alloc; ``` -通常の依存関係と異なり`Cargo.toml`を修正する必要はありません。その理由は、`alloc`クレートは標準ライブラリの一部としてRustコンパイラに同梱されているため、コンパイラはすでにこのクレートのことを知っているためです。この`extern crate`宣言を追加することで、コンパイラにこれをインクルードしようと試みるよう指定しています(昔はすべての依存関係が`extern crate`宣言を必要としていたのですが、いまは任意です)。 +通常の依存関係と異なり`Cargo.toml`を修正する必要はありません。その理由は、`alloc`クレートは標準ライブラリの一部としてRustコンパイラに同梱されているため、コンパイラはすでにこのクレートのことを知っているからです。この`extern crate`宣言を追加することで、コンパイラにこれをインクルードしようと試みるよう指定しています(昔はすべての依存関係が`extern crate`宣言を必要としていたのですが、いまは任意です)。
-**訳注:** もう少し詳しく解説します。Rust 2018から通常のクレートに関しては`extern crate`が必要なくなりましたが、Rust自体に同梱されるsysrootと呼ばれるクレートに関しては、特殊な状況でしか必要ないものがあるため明示的な`extern crate`がない限りリンクされません。詳しくは、[edition guideの対応するページ](https://doc.rust-jp.rs/edition-guide/rust-2018/path-changes.html#%E3%81%95%E3%82%88%E3%81%86%E3%81%AA%E3%82%89extern-crate)をご覧ください。 +**訳者注:** 詳しくは[edition guideの対応するページ](https://doc.rust-jp.rs/edition-guide/rust-2018/path-changes.html#%E3%81%95%E3%82%88%E3%81%86%E3%81%AA%E3%82%89extern-crate)をご覧ください。
@@ -229,18 +229,21 @@ build-std = ["core", "compiler_builtins", "alloc"] これでコンパイラは`alloc`クレートを再コンパイルして私たちのカーネルにインクルードしてくれます。 -`alloc`クレートが`#[no_std]`なクレートで標準では無効化されている理由は、これが追加の要件を持っているからです。今私たちのプロジェクトをコンパイルしようとすると、その要件が何なのか分かります: +`alloc`クレートが`#[no_std]`なクレートで標準では無効化されている理由は、これが追加の要件を持っているからです。今私たちのプロジェクトをコンパイルしようとすると、その要件をエラーとして目にすることになります: ``` error: no global memory allocator found but one is required; link to std or add #[global_allocator] to a static item that implements the GlobalAlloc trait. +(エラー:グローバルメモリアロケータが見つかりませんが、一つ必要です。 + stdをリンクするか、GlobalAllocトレイトを実装するスタティックな要素に#[global_allocator]を付けてください。) error: `#[alloc_error_handler]` function required, but not found +(エラー:`#[alloc_error_handler]`関数が必要ですが、見つかりません) ``` -最初のエラーは、`alloc`クレートが、ヒープアロケータという`allocate`と`deallocate`関数を提供するオブジェクトを必要とするために発生します。Rustにおいては、ヒープアロケータの性質は[`GlobalAlloc`]トレイトによって定義されており、エラーメッセージでもそのことについて触れられています。クレートにヒープアロケータを設定するためには、`#[global_allocator]`属性を`GlobalAlloc`トレイトを実装する何らかの`static`変数に適用する必要があります。 +最初のエラーは、`alloc`クレートが、ヒープアロケータという`allocate`と`deallocate`関数を提供するオブジェクトを必要とするために発生します。Rustにおいては、ヒープアロケータ(の満たすべき性質)は[`GlobalAlloc`]トレイトによって記述されており、エラーメッセージでもそのことについて触れられています。クレートのヒープアロケータを設定するためには、`#[global_allocator]`属性を`GlobalAlloc`トレイトを実装する何らかの`static`変数に適用する必要があります。 -二つ目のエラーは、(主にメモリが不足している場合)`allocate`への呼び出しが失敗しうるために発生します。私たちのプログラムがこのケースに対処できるようになっている必要があり、そのために使われる関数が`#[alloc_error_handler]`なのです。 +二つ目のエラーは、(主にメモリが不足している場合)`allocate`の呼び出しが失敗しうるために発生します。私たちのプログラムはこのケースに対処できるようになっている必要があり、そのために使われる関数が`#[alloc_error_handler]`なのです。 [`GlobalAlloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html @@ -277,8 +280,8 @@ pub unsafe trait GlobalAlloc { このトレイトは[`alloc_zeroed`]と[`realloc`]という二つのデフォルト実装付きメソッドも定義しています。 -- [`alloc_zeroed`]メソッドは`alloc`を呼んでから割り当てられたメモリブロックの値を0にするのに等しく、デフォルト実装もまさに同じことをしています。より効率的なカスタム実装がもしあるならば、デフォルト実装を上書きすることもできます。 -- [`realloc`]メソッドは割り当てたメモリを拡大したり縮小したりすることができます。デフォルト実装では、要求されたサイズの新しいメモリブロックを割り当て、以前のアロケーションから中身を全てコピーします。同じく、アロケータの実装でこのメソッドをより効率的にすることができるかもしれません。例えば、可能な場合はその場でアロケーションを拡大・縮小するなど。 +- [`alloc_zeroed`]メソッドは`alloc`を呼んでから割り当てられたメモリブロックの値を0にするのに等しく、デフォルト実装でもまさに同じことをしています。もし、より効率的なカスタム実装があるならば、デフォルト実装を上書きすることもできます。 +- [`realloc`]メソッドは割り当てたメモリを拡大したり縮小したりすることができます。デフォルト実装では、要求されたサイズの新しいメモリブロックを割り当て、以前のアロケーションから中身を全てコピーします。同じく、アロケータの実装によってはこのメソッドをより効率的に実装することができるかもしれません。例えば、可能な場合はその場でアロケーションを拡大・縮小するなど。 [`alloc_zeroed`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.alloc_zeroed [`realloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.realloc @@ -287,8 +290,8 @@ pub unsafe trait GlobalAlloc { トレイト自体とすべてのトレイトメソッドが`unsafe`として宣言されていることに気をつけましょう: -- トレイトを`unsafe`として宣言する理由は、プログラマがアロケータ型のトレイト実装が正しいことを保証しなければならないからです。例えば、`alloc`メソッドは他のどこかですでに使用されているメモリブロックを決して返してはならず、もしそうなると未定義動作が発生してしまいます。 -- 同様に、メソッドが`unsafe`である理由は、メソッドを呼び出す際に呼び出し元がいくつかの不変条件を保証しなければならないからです。例えば、`alloc`に渡される`Layout`が非ゼロサイズを指定していることなどです。実際にはこれは大して重要ではなく、というのもこれらのメソッドはコンパイラによって直接呼び出されるため、この要件が満たされていることは保証されているからです。 +- トレイトを`unsafe`として宣言する理由は、プログラマがアロケータ型のトレイト実装が正しいことを保証しなければならないからです。例えば、`alloc`メソッドは他のどこかですでに使用されているメモリブロックを決して返してはならず、もしそうすると未定義動作が発生してしまいます。 +- 同様に、メソッドが`unsafe`である理由は、メソッドを呼び出す際に呼び出し元がいくつかの不変条件を保証しなければならないからです。例えば、`alloc`に渡される`Layout`の指定するサイズが非ゼロであることなどです。実際にはこれは大して重要ではなく、というのもこれらのメソッドはコンパイラによって直接呼び出されるため、これらの要件が満たされていることは保証されているからです。 ### `DummyAllocator` @@ -300,7 +303,7 @@ pub unsafe trait GlobalAlloc { pub mod allocator; ``` -私たちのダミーアロケータではトレイトを実装するための最小限のことしかせず、`alloc`が呼び出されたら常にエラーを返すようにします。以下のようになります: +私たちのダミーアロケータでは、トレイトを実装するための最小限のことしかせず、`alloc`が呼び出されたら常にエラーを返すようにします。以下のようになります: ```rust // in src/allocator.rs @@ -321,11 +324,11 @@ unsafe impl GlobalAlloc for Dummy { } ``` -この構造体はフィールドを必要としないので、[サイズがゼロの型][zero sized type]として作成します。上で述べたように、この`alloc`では常に割り当てエラーに相当するヌルポインタを返すようにします。アロケータがメモリを返すことは絶対に起きないのだから、`dealloc`の呼び出しも絶対に起きないはずです。このため`dealloc`メソッドでは単にpanicすることにします。`alloc_zeroed`と`realloc`メソッドにはデフォルト実装があるので、これらを実装する必要はありません。 +この構造体はフィールドを必要としないので、[サイズがゼロの型][zero sized type]として作成します。上で述べたように、`alloc`は常に割り当てエラーに相当するヌルポインタを返すようにします。アロケータがメモリを返すことは絶対に起きないのだから、`dealloc`の呼び出しも絶対に起きないはずです。このため`dealloc`メソッドでは単にpanicすることにします。`alloc_zeroed`と`realloc`メソッドにはデフォルト実装があるので、これらを実装する必要はありません。 [zero sized type]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts -単純なアロケータを手に入れたわけですが、さらにRustコンパイラにこのアロケータを使うよう指示しないといけません。ここで`#[global_allocator]`属性の出番です。 +こうして単純なアロケータを手に入れたわけですが、さらにRustコンパイラにこのアロケータを使うよう指示しないといけません。ここで`#[global_allocator]`属性の出番です。 ### `#[global_allocator]`属性 @@ -348,9 +351,9 @@ error: `#[alloc_error_handler]` function required, but not found ### `#[alloc_error_handler]`属性 -`GlobalAlloc`トレイトについて議論したときに学んだように、`alloc`関数はヌルポインタを返すことによって割り当てエラーを示します。ここで生じる疑問は、そのように割り当てが失敗したときRustランタイムはどう対処するべきなのかということです。ここで`#[alloc_error_handler]`属性の出番です。この属性は、パニックが起こったときに私たちのパニックハンドラが呼ばれるのと同様に、割り当てエラーが起こったときに呼ばれる関数を指定するのです。 +`GlobalAlloc`トレイトについて議論したときに学んだように、`alloc`関数はヌルポインタを返すことによって割り当てエラーを示します。ここで生じる疑問は、そのように割り当てが失敗したときRustランタイムはどう対処するべきなのかということです。ここで`#[alloc_error_handler]`属性の出番です。この属性は、パニックが起こったときにパニックハンドラが呼ばれるのと同じように、割り当てエラーが起こったときに呼ばれる関数を指定するのです。 -コンパイルエラーを修正するためにそんな関数を追加してみましょう: +コンパイルエラーを修正するためにそのような関数を追加してみましょう: ```rust // in src/lib.rs @@ -363,7 +366,7 @@ fn alloc_error_handler(layout: alloc::alloc::Layout) -> ! { } ``` -`alloc_error_handler`関数はまだunstableなので、この機能 (feature) を有効化する必要があります。この関数は引数を一つ取ります:割り当てエラーが起こったとき`alloc`関数に渡されていた`Layout`のインスタンスです。この失敗を解決するためにできることはないので、`Layout`インスタンスを含めたメッセージを表示してただpanicすることにしましょう。 +`alloc_error_handler`関数はまだunstableなので、feature gateによってこれを有効化する必要があります。この関数は引数を一つ取ります:割り当てエラーが起こったとき`alloc`関数に渡されていた`Layout`のインスタンスです。割り当ての失敗を解決するためにできることはないので、`Layout`インスタンスを含めたメッセージを表示してただpanicすることにしましょう。 この関数を追加したことで、コンパイルエラーは修正されたはずです。これで`alloc`のアロケーション・コレクション型を使えるようになりました。例えば、[`Box`]を使ってヒープに値を割り当てることができます: @@ -395,11 +398,11 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ![QEMU printing "panicked at `allocation error: Layout { size_: 4, align_: 4 }, src/lib.rs:89:5"](qemu-dummy-output.png) -`Box::new`関数は暗黙のうちにグローバルアロケータの`alloc`関数を呼び出すため、エラーハンドラが呼ばれました。私たちのダミーアロケータは常にヌルポインタを返すので、あらゆる割り当ては失敗します。これを修正するためには、使用可能なメモリを実際に返すアロケータを作る必要があります。 +`Box::new`関数は暗黙のうちにグローバルアロケータの`alloc`関数を呼び出すため、エラーハンドラが呼ばれました。私たちのダミーアロケータは常にヌルポインタを返すので、あらゆる割り当てが失敗するのです。これを修正するためには、使用可能なメモリを実際に返すアロケータを作る必要があります。 ## Creating a Kernel Heap -適当なアロケータを作る前にまず、そのアロケータがメモリを割り当てるためのヒープメモリ領域を作らないといけません。このために、ヒープ領域のための仮想メモリ範囲を定義し、その領域を物理フレームに対応付ける必要があります。仮想メモリとページテーブルの概要については、[ページング入門][_"Introduction To Paging"_]の記事を読んでください。 +適切なアロケータを作りたいですが、その前にまず、そのアロケータがメモリを割り当てるためのヒープメモリ領域を作らないといけません。このために、ヒープ領域のための仮想メモリ範囲を定義し、その領域を物理フレームに対応付ける必要があります。仮想メモリとページテーブルの概要については、[ページング入門][_"Introduction To Paging"_]の記事を読んでください。 [_"Introduction To Paging"_]: @/edition-2/posts/08-paging-introduction/index.md @@ -455,7 +458,7 @@ pub fn init_heap( } ``` -この関数は[`Mapper`]と[`FrameAllocator`]への可変参照を取ります。引数はどちらも[`Size4KiB`]をジェネリックパラメータとすることで4KiBページのみに制限しています。この関数の戻り値は[`Result`]で、成功ヴァリアントが`()`、失敗ヴァリアントが([`Mapper::map_to`]メソッドによって失敗時に返されるエラー型である)[`MapToError`]です。このエラー型を流用するのは理にかなっており、というのもこの関数における主なエラーの原因は`map_to`メソッドだからです。 +この関数は[`Mapper`]と[`FrameAllocator`]への可変参照を取ります。これらはどちらも[`Size4KiB`]をジェネリックパラメータとすることで4KiBページのみに制限しています。この関数の戻り値は[`Result`]で、成功ヴァリアントが`()`、失敗ヴァリアントが([`Mapper::map_to`]メソッドによって失敗時に返されるエラー型である)[`MapToError`]です。この関数における主なエラーの原因は`map_to`メソッドであるため、このエラー型を流用するのは理にかなっています。 [`Mapper`]:https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html [`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html @@ -466,13 +469,13 @@ pub fn init_heap( 実装内容は以下の二つに分けられます: -- **ページ範囲の作成:** 対応付けたいページ領域を作成するために、ポインタ`HEAP_START`を[`VirtAddr`]型に変換します。つぎに`HEAP_SIZE`を足すことによってヒープの終端アドレスを計算します。端が含まれる境界 (インクルーシブレンジ) にしたい(ヒープの最後のバイトのアドレスが欲しい)ので1を引きます。次に、これらのアドレスを[`containing_address`]関数を使って[`Page`]型に変換します。最後に、[`Page::range_inclusive`]関数を使って最初と最後のページからページ範囲を作成します。 +- **ページ範囲の作成:** 対応付けたいページ領域を作成するために、ポインタ`HEAP_START`を[`VirtAddr`]型に変換します。つぎに`HEAP_SIZE`を足すことによってヒープの終端アドレスを計算します。端が含まれる境界 (インクルーシブレンジ) にしたい(ヒープの最後のバイトのアドレスとしたい)ので1を引きます。次に、これらのアドレスを[`containing_address`]関数を使って[`Page`]型に変換します。最後に、[`Page::range_inclusive`]関数を使って最初と最後のページからページ範囲を作成します。 - **ページの対応付け (マッピング) :** 二つ目のステップは、今作ったページ範囲のすべてのページに対して対応付けを行うことです。これを行うため、`for`ループを使ってこのページ範囲に対して繰り返し処理を行います。それぞれのページに対して以下を行います: - [`FrameAllocator::allocate_frame`]メソッドを使って、ページのマップされるべき物理フレームを割り当てます。このメソッドはもうフレームが残っていないとき[`None`]を返します。このケースに対処するため、[`Option::ok_or`]メソッドを使ってこれを[`MapToError::FrameAllocationFailed`]に変換し、エラーの場合は[`?`演算子][question mark operator]を使って早期リターンしています。 - - このページに対し、必要となる`PRESENT`フラグと`WRITABLE`フラグをセットします。これらのフラグにより読み書きのアクセスが許可され、ヒープメモリとして理にかなっています。 + - このページに対し、必要となる`PRESENT`フラグと`WRITABLE`フラグをセットします。これらのフラグにより読み書きのアクセスが許可されますが、これはヒープメモリとして理にかなっています。 - [`Mapper::map_to`]メソッドを使ってアクティブなページテーブルに対応付けを作成します。このメソッドは失敗しうるので、同様に[`?`演算子][question mark operator]を使ってエラーを呼び出し元に受け渡します。成功時には、このメソッドは[`MapperFlush`]インスタンスを返しますが、これを使って[`flush`]メソッドを呼ぶことで[**トランスレーション・ルックアサイド・バッファ**][_translation lookaside buffer_]を更新することができます。 @@ -520,15 +523,15 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -ここで、文脈が分かるよう関数の全体を示しています。新しい行は`blog_os::allocator`のインポートと`allocator::init_heap`の呼び出しだけです。`init_heap`関数がエラーを返した場合、これを処理する賢い方法は今のところないため、[`Result::expect`]メソッドを使ってパニックします。 +ここで、文脈が分かるよう関数の全体を示しています。(しかし)新しい行は`blog_os::allocator`のインポートと`allocator::init_heap`の呼び出しだけです。`init_heap`関数がエラーを返した場合、これを処理する良い方法は今のところないため、[`Result::expect`]メソッドを使ってパニックします。 [`Result::expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect -これで、使用する準備のできた、対応付けられたヒープメモリ領域を手に入れました。`Box::new`の呼び出しはまだ私たちの古い`Dummy`を使っているので、実行しても依然として「メモリ切れ」のエラーを見ることになるでしょう。適切なアロケータを使うようにして、このエラーを修正してみましょう。 +これで、使用する準備のできた、対応付けられたヒープメモリ領域を手に入れました。`Box::new`の呼び出しはまだ私たちの古い`Dummy`アロケータを使っているので、実行しても依然として「メモリ不足」のエラーを見ることになるでしょう。適切なアロケータを使うようにして、このエラーを修正してみましょう。 ## アロケータクレートを使う -アロケータを実装するのは少々複雑なので、まずは既製のアロケータを使うことにしましょう。自前のアロケータを実装する方法については次の記事で学びます。 +アロケータを実装するのは少々複雑なので、まずは既製のアロケータを使うことにしましょう。アロケータを自作する方法については次の記事で学びます。 `no_std`のアプリケーションのためのシンプルなアロケータのひとつに[`linked_list_allocator`]クレートがあります。この名前は、割り当てられていないメモリ領域を連結リストを使って管理しているところから来ています。この手法のより詳しい説明については次の記事を読んでください。 @@ -554,7 +557,7 @@ use linked_list_allocator::LockedHeap; static ALLOCATOR: LockedHeap = LockedHeap::empty(); ``` -この構造体は同期のために`spinning_top::Spinlock`型を使うため`LockedHeap`という名前が付いています。同期が必要なのは、静的変数`ALLOCATOR`に複数のスレッドが同時にアクセスすることがありえるからです。スピンロックやmutexを使うときはいつもそうであるように、誤ってデッドロックを起こさないように注意する必要があります。これが意味するのは、我々は割り込みハンドラ内で一切アロケーションを行ってはいけないと言うことです。なぜなら、割り込みハンドラはどんなタイミングでも走る可能性があるため、進行中のアロケーションに割り込んでいることがあるからです。 +この構造体は同期のために`spinning_top::Spinlock`型を使うため`LockedHeap`という名前が付いています。これが必要なのは、`ALLOCATOR`静的変数に複数のスレッドが同時にアクセスすることがありえるからです。スピンロックやmutexを使うときはいつもそうであるように、誤ってデッドロックを起こさないように注意する必要があります。これが意味するのは、我々は割り込みハンドラ内で一切アロケーションを行ってはいけないと言うことです。なぜなら、割り込みハンドラはどんなタイミングでも走る可能性があるため、進行中のアロケーションに割り込んでいることがあるからです。 [`spinning_top::Spinlock`]: https://docs.rs/spinning_top/0.1.0/spinning_top/type.Spinlock.html @@ -569,7 +572,7 @@ pub fn init_heap( mapper: &mut impl Mapper, frame_allocator: &mut impl FrameAllocator, ) -> Result<(), MapToError> { - // […] map all heap pages to physical frames + // […] すべてのヒープページを物理フレームにマップする // new unsafe { @@ -580,13 +583,13 @@ pub fn init_heap( } ``` -`LockedHeap`型の内部のスピンロックの[`lock`]メソッドを呼ぶことで、ラップされた[`Heap`]インスタンスへの排他的参照を得て、これの[`init`]メソッドをヒープの境界を引数として呼んでいます。`init`関数自体がヒープメモリに書き込もうとするので、ヒープページを対応付けた **後に** ヒープを初期化することが重要です。 +`LockedHeap`型の内部のスピンロックの[`lock`]メソッドを呼ぶことで、ラップされた[`Heap`]インスタンスへの排他参照を得て、これの[`init`]メソッドをヒープの境界を引数として呼んでいます。`init`関数自体がヒープメモリに書き込もうとするので、ヒープページを対応付けた **後に** ヒープを初期化することが重要です。 [`lock`]: https://docs.rs/lock_api/0.3.3/lock_api/struct.Mutex.html#method.lock [`Heap`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html [`init`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.init -ヒープを初期化できたら、組み込みの[`alloc`]クレートのあらゆるアロケーション・コレクション型がエラーなく使用できます: +ヒープを初期化できたら、組み込みの[`alloc`]クレートのあらゆるアロケーション・コレクション型がエラーなく使用できます: ```rust // in src/main.rs @@ -620,7 +623,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -このコード例では[`Box`], [`Vec`], [`Rc`]型を使ってみせています。`Box`型と`Vec`型については対応するヒープポインタを[`{:p}`フォーマット指定子][`{:p}` formatting specifier]を使って出力しています。`Rc`について例示するために、参照カウントされたヒープ値を作成し、インスタンスを([`core::mem::drop`]を使って)ドロップする前と後に[`Rc::strong_count`]関数を使って現在の参照カウントを出力しています。 +このコード例では[`Box`], [`Vec`], [`Rc`]型を使ってみました。`Box`型と`Vec`型については対応するヒープポインタを[`{:p}`フォーマット指定子][`{:p}` formatting specifier]を使って出力しています。`Rc`についての例を示すために、参照カウントされたヒープ値を作成し、インスタンスを([`core::mem::drop`]を使って)ドロップする前と後に[`Rc::strong_count`]関数を使って現在の参照カウントを出力しています。 [`Vec`]: https://doc.rust-lang.org/alloc/vec/ [`Rc`]: https://doc.rust-lang.org/alloc/rc/ @@ -637,9 +640,9 @@ current reference count is 2 reference count is 1 now ](qemu-alloc-showcase.png) -期待したとおり、`Box`と`Vec`の値はヒープ上にあり、そのことはポインタが`0x_4444_4444_*`で始まることから分かります。参照カウントされた値も期待したとおり振る舞っており、`clone`呼び出しの後では参照カウントは2に、インスタンスの一方がドロップされた後では再び1になっています。 +ポインタが`0x_4444_4444_*`で始まることから、`Box`と`Vec`の値は想定通りヒープ上にあることが分かります。参照カウントされた値も期待したとおり振る舞っており、`clone`呼び出しの後では参照カウントは2になり、インスタンスの一方がドロップされた後では再び1になっています。 -ベクタがヒープメモリの先頭から`0x800`だけずれた場所から始まるのは、Box内の値が`0x800`バイトの大きさがあるためではなく、ベクタが容量を増やさなければならないときに発生する[再割り当て (リアロケーション) ][reallocations]のためです。例えば、ベクタの容量が32の際に次の要素を追加しようとすると、ベクタは内部で容量64の配列を新たに割り当て、すべての要素をコピーしているのです。その後古い割り当てを解放します。 +ベクタがヒープメモリの先頭から`0x800`だけずれた場所から始まるのは、Box内の値が`0x800`バイトの大きさがあるためではなく、ベクタが容量を増やさなければならないときに発生する[再割り当て (リアロケーション) ][reallocations]のためです。例えば、ベクタの容量が32の際に次の要素を追加しようとすると、ベクタは内部で容量64の配列を新たに割り当て、すべての要素をコピーします。その後古い割り当てを解放しています。 [reallocations]: https://doc.rust-lang.org/alloc/vec/struct.Vec.html#capacity-and-reallocation @@ -665,7 +668,7 @@ reference count is 1 now ## テストを追加する -私たちの新しい割り当てコードを間違って壊してしまうことがないことを保証するために、結合 (インテグレーション) テストを追加しなければなりません。まず、次のような内容のファイル`tests/heap_allocation.rs`を作成します。 +いま新しく作ったアロケーションコードを間違って壊してしまうことがないことを保証するために、結合 (インテグレーション) テストを追加するべきでしょう。まず、次のような内容のファイル`tests/heap_allocation.rs`を作成します。 ```rust // in tests/heap_allocation.rs @@ -721,9 +724,9 @@ fn main(boot_info: &'static BootInfo) -> ! { } ``` -私たちの`main.rs`内の`kernel_main`関数によく似ていますが、`println`を呼び出さず、例として行ったアロケーションも行わず、また`test_main`を無条件で呼び出しているという違いがあります。 +私たちの`main.rs`内の`kernel_main`関数によく似ていますが、`println`を呼び出さず、例示のため行ったアロケーションも行わず、また`test_main`を無条件で呼び出しているという違いがあります。 -これでいくつかテストケースを追加する準備ができました。まず、[`Box`]を使って単純な割り当て (アロケーション) を行い、割り当てられた値を確かめることで基本的なアロケーションがうまくいっていることを確かめましょう: +これでテストケースを追加する準備ができました。まず、[`Box`]を使って単純な割り当て (アロケーション) を行い、割り当てられた値を確かめることで基本的なアロケーションがうまくいっていることを確かめるテストを追加しましょう: ```rust // in tests/heap_allocation.rs @@ -740,7 +743,7 @@ fn simple_allocation() { 最も重要なのは、このテストはアロケーションエラーが起きないことを検証してくれるということです。 -次に、反復によって大きなベクタを作り、大きな割り当てと(再割り当てによる)複数回の割り当ての両方をテストしましょう: +次に、反復によって少しずつ大きなベクタを作ることで、大きな割り当てと(再割り当てによる)複数回の割り当ての両方をテストしましょう: ```rust // in tests/heap_allocation.rs @@ -758,11 +761,11 @@ fn large_vec() { } ``` -このベクタの和を[n次部分和][n-th partial sum]の公式と比較することで検証しています。これにより、割り当てられた値はすべて正しいことがある程度保証されます。 +このベクタの和を[n次部分和][n-th partial sum]の公式と比較することで検証しています。これにより、割り当てられた値はすべて正しいことをある程度保証できます。 [n-th partial sum]: https://en.wikipedia.org/wiki/1_%2B_2_%2B_3_%2B_4_%2B_%E2%8B%AF#Partial_sums -3つ目のテストとして、一万回次々にアロケーションを行います: +3つ目のテストとして、10000回次々にアロケーションを行います: ```rust // in tests/heap_allocation.rs @@ -778,7 +781,7 @@ fn many_boxes() { } ``` -このテストではアロケータが解放されたメモリを次の割り当てで再利用していることを保証してくれます。もしそうなっていなければ、メモリ不足が起きるでしょう。これはアロケータにとって当たり前の要件だと思われるかもしれませんが、これを行わないようなアロケータの設計も存在するのです。その例として、次の記事で説明するbump allocatorがあります。 +このテストではアロケータが解放されたメモリを次の割り当てで再利用していることを保証してくれます。もしそうなっていなければメモリ不足が起きるでしょう。こんなことアロケータにとって当たり前の要件だと思われるかもしれませんが、これを行わないようなアロケータの設計も存在するのです。その例として、次の記事で説明するbump allocatorがあります。 では、私たちの新しい結合テストを実行してみましょう: @@ -795,12 +798,12 @@ many_boxes... [ok] ## まとめ -この記事では動的メモリについて入門し、なぜ、そしていつそれが必要になるのかを説明しました。Rustの借用チェッカがどのようにしてよくある脆弱性を防ぐのか、そしてRustのアロケーションAPIの仕組みを理解しました。 +この記事では動的メモリに入門し、なぜ、そしていつそれが必要になるのかを説明しました。Rustの借用チェッカがどのようにしてよくある脆弱性を防ぐのか、そしてRustのアロケーションAPIがどのような仕組みなのかを理解しました。 ダミーアロケータでRustのアロケータインターフェースの最小限の実装を作成した後、私たちのカーネル用の適切なヒープメモリ領域を作成しました。これを行うために、ヒープ用の仮想アドレス範囲を定義し、前の記事で説明した`Mapper`と`FrameAllocator`を使ってその範囲のすべてのページを物理フレームに対応付けました。 -最後に、`linked_list_allocator`クレートへの依存関係を追加し、適切なアロケータを私たちのカーネルに追加しました。このアロケータのおかげで、`alloc`クレートに含まれる`Box`、`Vec`、その他のアロケーション・コレクション型を使えるようになりました。 +最後に、`linked_list_allocator`クレートへの依存関係を追加し、適切なアロケータを私たちのカーネルに追加しました。このアロケータのおかげで、`alloc`クレートに含まれる`Box`、`Vec`、その他のアロケーション・コレクション型を使えるようになりました。 ## 次は? -この記事でヒープ割り当て機能のサポートを追加したとは言え、ほとんどの仕事は`linked_list_allocator`クレートに任せてしまっています。次の記事では、アロケータをゼロから実装する方法を詳細にお伝えします。可能なアロケータの設計を複数示し、それらを単純化した者を実装する方法を示し、その利点と欠点を示します。 +この記事でヒープ割り当て機能のサポートを追加したとはいえ、ほとんどの仕事は`linked_list_allocator`クレートに任せてしまっています。次の記事では、アロケータをゼロから実装する方法を詳細にお伝えします。可能なアロケータの設計を複数提示し、それらを単純化したものを実装する方法を示し、それらの利点と欠点を説明します。