「ゼロからのOS自作入門」を Rust でやる (第7章)

引き続き Rust で OS を作っていきます。 今回は、Rust 1.39 で安定化されたあの機能が登場します!!!

第7章

割り込みハンドリングの章です。 謎の現象が起きてなかなか実装に苦労した章です。

割り込み契機でマウスカーソルを動かすようにする (day07a)

6章までの実装では無限ループでポーリングすることで xHC のイベント発生を検知していました。 本章では xHCI で定められた割り込みの発生方法である、 MSI (Message Signaled Interrupts) に対応し、 MSI 割り込みを受け取った契機でマウスカーソルを動かすようにします。

github.com

C++実装の通りに割り込みハンドラを定義し、 IDT (Interrupt Descriptor Table) を設定し、 PCI コンフィグレーション空間の読み書きで MSI を設定しました。

さっそくQEMUで動かしてみたところ、割り込み呼び出し箇所で Page Fault や Double Fault や Segment Not Present やらの例外が発生して正しく動作しません。 なにが起きているのかよく分からないのですが、各例外発生時の情報を見て想像するに、どうやらセグメントがおかしいのが原因のようです。

github.com

というわけで GDT (Global Descriptor Table) でセグメント設定するようにしてみました。 コードセグメントを設定するだけではだめだったので、スタックセグメントも設定したところ、うまく動作するようになりました。 理屈は全く理解できていませんが、ひとまず動くようになったのでヨシ!ということで先にすすめたいと思います。

ヒープへ対応

C++ 実装でヒープに対応するのは第9章ですが、この次に実装するものでヒープが必要だったので先にヒープを実装してしまいます。 C++ 実装では brk を実装することで newlib の malloc を利用できるようにしていたのですが、 Rust ではこれ以上 newlib に依存するのは避けたかったので、 "Writing an OS in Rust" の "Allocator Designs" の記事 を参考に独自のメモリアロケータを作成しました (ほぼほぼコピペですが...)。

github.com

このメモリアロケータ実装ではヒープ領域とするフレームの数と同じ回数だけ FrameAllocator::allocate() を呼び出します。 今回ヒープ領域は 128MiB としているので当該関数は 128MiB / 4KiB = 32,768 回呼び出されることになります。 FrameAllocator (BitmapMemoryManager) の実装そのままでは処理に時間が掛かりすぎ、OSの起動に長時間かかるようになってしまったため、 BitmapMemoryManager の実装を高速化しています。 具体的には、割り当て可能なフレームを探索する範囲を意味する BitmapMemoryAllocator::range の値を、フレームの割り当ての度に更新することで不要な探索を削減するようにしています。 (フレームの割り当て回数 N に対し、従来は N^ 2 / 2 回処理が発生していたところを N 回の処理で済むように改善しました)

これで Box や Rc や Arc が使えるようになりました。

BIOS で起動しなくなっていたのを修正する (修正できなかった)

ここまで開発してきた機能は主に UEFI のブートローダーで動作するか確認していたのですが、 BIOS のブートローダーで確認したところ、起動処理中に panic が発生するようになってしまっていましたので修正します。

github.com

一つ目の問題は、ブートローダーに渡されたメモリのマッピング情報から各フレームの使用状況を更新する処理にありました。 具体的には、BIOS のブートローダーから渡されるメモリのマッピング情報に含まれるアドレスがフレーム境界に揃っていなかったためエラーとなっていたのでした。 このアドレスをフレーム境界に合うように丸めることで問題に対処しました。 なお、他の用途で使われているフレームを使用可能として扱ってはいけないため、フレーム全体が使用可能なフレームのみ使用可能としています。

これで一つ目の問題は解決したのですが、依然として C++ 側処理のログ出力処理で Segment Not Present の例外が発生してしまいます。 どうも浮動小数関連のレジスタを操作している箇所が問題で、 va_start などが関係していそうな気がするのですが、原因がよく分からないですし、 UEFI 側では問題なく動作しているようなので、ひとまず放っておくことにします。

async/await を使えるようにする

"Writing an OS in Rust" の "Async/Await" の章 に感銘を受けたので、 sabios にも実装します。 割り込み契機のイベント処理のために async/await が使えると非常に便利で綺麗に書けると思います。 これがやりたかったがために MikanOS の Rust 移植を始めたと言っても過言ではありません。

github.com

Executor 等の実装は "Writing an OS in Rust" のほぼコピペです。 これだけのコードで非同期ランタイムが作れてしまうのはすごいですよね。

タスクを意味する構造体の名前は、 CoTask (Cooperative Task)としました。 Task ではなく CoTask としたのは、後の章で出てくるプリエンプティブなタスクと区別するためです。

async/await で割り込みを処理する (day07b)

割り込みハンドラ内でイベント処理を行っていたのを、割り込みハンドラ内ではイベントを通知しメインのタスクで通知を受け取りイベント処理するように変更する節です。 Rust 実装ではグローバルなイベントキューではなく async/await を使います!

github.com

今回は割り込みが発生したか否かという情報だけを割り込みハンドラから CoTask へ通知すれば良いので、キューではなく AtomicBool で割り込み発生有無を通知するようにしました。 Ordering は Relaxed で良い...はず... (あまり自信がないです)

github.com

ついでにマウスカーソル移動時のイベント処理についても CoTask にしました。 従来処理では C++ から呼び出されるコールバック関数内で mouse_cursor や framebuffer のロックを取得していたため、ロック順序の整合性がとれているか不安だったのですが、今回の改造によりコールバック関数の中ではロックを取得しなくなったので、安心度が上がりました。

まとめ

割り込みに対応し、割り込み契機でマウスカーソルを動かせるようにしました。 また、 async/await で割り込みをエレガントに処理できるようにしました。 一方、BIOS のブートローダーではなぜか動作しなくなってしまいました。謎です。。。

さて、8章の内容は先日やってしまったので、次は9章です。 どんどん OS が高機能化していって Rust で実装する楽しみが増えそうですね! 次回もお楽しみに。