借り初めのひみつきち

仮ブログです。

Rust 自作 OS 日記/Part 5 マルチタスキング

古代のコンピュータでは1台のコンピュータでひとつのタスクを実行するのが普通でした。*1 現代のコンピュータでは1台のコンピュータで複数のタスクを同時に実行するのが当たり前となり、さらには複数の OS を並列に動かす技術さえあります。

複数のタスクを同時実行するためには、タスクの間を調停する機能が必要となります。

マルチタスキングの種類

ハードウェアでマルチタスキングを実現する機能としてマルチプロセッシング (SMP: Symmetric Multiprocessing) やハードウェアマルチスレッディング (SMT: Simultaneous Multi-Threading /HTTなど) があり、これは既に myos にも実装されています。いずれ詳しく取り上げるかもしれません。 一方、ソフトウェアでマルチタスキングを実現する方法は大きく分けると2種類あってそれぞれプリエンプティブマルチタスキングと協調的マルチタスキングと呼ばれます。

プリエンプティブマルチタスキングはタイマー割り込みなどを使ってごく短時間で実行するタスクを次々切り替える方式です。 現在実行中のタスクの実行権を強制的に取り上げて他のタスクに切り替えることをプリエンプションと言います。 実行中のタスクがどんな状態でも強制的に他のタスクに切り替えできることから応答性は良くなりますが、タスク切り替えのタイミングが把握しづらく、中途半端な状態で切り替わっても問題ないように排他処理などの実装のコストは大きくなります。

もう一方の協調的マルチタスキングはタスク側が明示的に実行権を明け渡した時にタスクを切り替える方式です。 メリットデメリットはプリエンプティブマルチタスキングの正反対で、実装コストが比較的小さくタスク側でタスク切り替えのタイミングを制御できる反面、ひとつのタスクが長時間実行されると他のタスクに切り替えできなくなって応答性が悪くなります。

現代の OS では主にプリエンプティブマルチタスキングを採用していますが、ハードウェアの I/O 待ち合わせ処理や GUI プログラミングにおけるイベントループ処理は一般的に協調的マルチタスキングかそれに近い動作をします。

myos でもこれに倣ってプリエンプティブマルチタスキングを実装していますが、協調的マルチタスキングを採用することで効率よく処理できるタスクも多く存在するため対応したいです。

幸いにして Rust には async / await という協調的マルチタスキングをサポートする言語機能が備わっているため、これを利用して効率的なタスク管理を実装します。

async / await

Rust の協調的マルチタスキングは Future という trait を実装したオブジェクトを使います。 協調動作する関数には async という特別なキーワードを付けて定義すると関数のコンテキスト (変数などの状態) を保存するために内部的な struct を作って dyn Future に変換します。 また、 await キーワードは async 関数の中でのみ利用でき、 await キーワードが現れた前後で関数のコンテキスト用 struct を分離してタスクを中断できるようにしています。

実は Rust が言語としてサポートしているのはここまでで、実際のタスクをどのように管理して実行するかは外部のライブラリに任されています。

いくつかメジャーなライブラリがあるようですが、自作 OS ですし Executor 自体は簡単なサンプルがあるのでそれをもとに実装します。

いざ実装

myos の場合、以下のようなオブジェクトが登場します。

Task

dyn Future に一意の TaskId を付けてラッピングしたもので、個別のタスクに相当します。

Executor

タスクの管理と実行を行う中心部です。 最も単純な Executor は配列に入れた dyn Future に対して順番に poll を呼び出し、 poll の中ではタスクが完了したら Poll::Ready<T> を、タスクが待ち状態に入ったら Poll::Pending を返すのを繰り返します。 myos の Executor は各スレッドにひとつ持つことができます。

TaskWaker

await で待ち状態に入ったタスクに対して再開可能になったことを通知します。

よかったね

ここまで実装してウィンドウメッセージの処理は async / await で処理できるようになり、ひとつのスレッドで複数のウィンドウのメッセージループを処理することができるようになりました。

f:id:neriring16:20201106001952p:plain ↑何が変わったか伝わりづらいスクリーンショット

実はタイマーとスケジューラーの内部処理に問題があってそっちの対応の方が手間がかかってました笑

*1:大規模なコンピュータではタイムシェアリングなどありましたが

Rust 自作 OS 日記/Part 4.1 おまけ

前回の日記でひとつ触れ忘れました。

Rust 自作 OS 日記/Part 4 フォントのはなし - 借り初めのひみつきち

ベクターフォントのラスターフォントにないもうひとつの利点、 それは描画予定のサイズよりあらかじめ大きいバッファに描画してから縮小して実際の画面に描画することでなめらかなフォントを描画できます。

やり方はいろいろ考えられますが、直線のアルゴリズムがブレゼンハムでもこの程度の描画は可能です。

f:id:neriring16:20201016224911p:plain

前回より見やすくなってるのではないでしょうか?

座標を実数で扱ったりサブピクセルレンダリングなどを利用するとさらに高品質に描画できる可能性があります。

Rust 自作 OS 日記/Part 4 フォントのはなし

古きよき太古のコンピューターは ROM にフォントを内蔵していたので文字コードを指定するだけで画面に文字を表示することができました。 現代のコンピューターは広大な画面に自由に図形を描画できるようになった代償として文字を表示するためにフォントが必要になりました。

ラスターフォントとベクターフォント

さて、フォントデータの形式は大きく分けるとラスターフォントとベクターフォントの2種類に大別されます。

ラスターフォントはラスタライズされたピクセルの集合で表現します。 メリットはコンピューターの画面もほぼ同じ仕組みで画像を表示しているので表示するための処理が簡単なところです。 デメリットはあらかじめ設計された固定サイズの表示が基本となり、拡大や縮小は無理やりアルゴリズムで補完してもあまり綺麗な表示はできません。 古いコンピューターや組み込み用途でよく使われます。自作 OS も大半はこの方式のフォントです。

ベクターフォントはペンで書くための座標の軌跡の組み合わせのようなデータ表現です。 メリットは座標の組み合わせのデータなので拡大縮小が容易です。ただし、小さいサイズや極端に大きいサイズはそのままではあまり綺麗に描画できません。 デメリットは画面に表示するためにペンを動かして描画する必要があるため処理が複雑になります。 現代の OS の GUI フォントは基本こちらを利用します。

このようにふたつのフォント形式はほぼ反対の性質を持っているので適材適所で使い分けされます。

コンソール用途ではラスターフォントで十分ですが、 GUI の画面はさまざまなフォントが必要になってくるためラスターフォントでは対応するのが困難になってきます。ベクターフォントが欲しいところです。

Hershey フォント

ベクターフォントといえば TrueType や OpenType が有名ですね。 こちらもそのうち対応したいところですが、仕様が複雑すぎていきなり実装は無理なので良いライブラリを探してくる必要がありそうです。

ところで、実はもっと簡単に対応できるフォントがあります。

Hershey fonts - Wikipedia

このフォントは座標のセットがテキストでエンコードされており、デコードして直線で繋ぐだけで描画できます。 理論上、 32 x 32 くらいのサイズを超えるフォントは実現不可能で曲線も表現できないのでそれほど複雑なフォントは表現できないですが、 それでもベクターフォントなのである程度の拡大縮小は自在です。

私の OS

そんなわけで私の OS にも実装してみました。

f:id:neriring16:20201013202656p:plain

f:id:neriring16:20201013202714p:plain

f:id:neriring16:20201013202725p:plain

このようにさまざまなフォントがさまざまなサイズで描画されて、だいぶそれっぽくなってきました。

見た目だけなら moe に近づいてきたか、もう追い越してる感じがしてきました。

ほんとのところは別のところに手をつけたかったですが、さいきん時間があまり取れなくて日記を書くのが遅れてしまいました(/ _ ; )

ACPI 2.0

いまどきの PC 用 OS では ACPI 対応を避けて通ることができません。

現在、私の OS では以下の ACPI ライブラリを利用しています。

GitHub - rust-osdev/acpi: Rust library for parsing ACPI tables and AML

ACPI には様々なテーブルがありますが、このライブラリはいくつかの決まったテーブルの内容を Rust の構造体に変換するだけで、 ACPI にはもっとたくさんのテーブルがあるのに使えるインターフェースが用意されていませんでした。

割と不満がありつつも ACPI は仕様が壮大すぎて自前で実装するのが面倒だったので、本当に困るまでは当面そのまま使うことにしました。

そして、このライブラリが最近アップデートがあって 2.0 になったようです🎉

ついに ACPI テーブルを検索する機能が実装されました!

f:id:neriring16:20201005232927p:plain

ということで早速使ってみましょう。もちろん BGRT テーブル*1 を使います。

ババーン!

f:id:neriring16:20201005233702p:plain

なんだよ private って💢

検索メソッドがあるのに Signature などの重要な構造体が外から見えない設定になってるので結局使えませんでした😿

ということで、ローカルにコピーしてサクッと修正してみました。

f:id:neriring16:20201005234549p:plain

ちゃんとロゴが表示できました✨

バグレポートどうやって出せばいいんだろ

Rust 自作 OS 日記/Part 3 ブートローダー

最近の大きな変更点は、カーネルブートローダーを分離しました!

f:id:neriring16:20200815140338p:plain

UEFI の便利なところはカーネルを直接起動できるところです。 一方、カーネルUEFI のコードが含まれることによる問題も少なからずあるので、 ブートローダーを分離することになりました。

これにより以下のような拡張の余地が生まれます(対応するとは言ってない( ー`дー´)キリッ

  • UEFI 以外のファームウェアからの起動
  • ビット数が異なる(32bit) UEFI からの起動
  • 自由なページング、自由なカーネルの配置、ASLR
  • バイナリ形式の変更(ELFなど) 、圧縮など
  • ローダーとカーネルで独立したメモリアロケーター

移行手順としては、まず以前 C で作ったブートローダーから起動できるようにカーネルの起動部分を調整、その後 Rust でほぼ同等機能のブートローダーを書き直し、両者の IF を調整しながらカーネルから UEFI 依存コードを排除という手順で移行しました。

qemu で動くようになっていざ実機で試したら NX bit のせいで起動しませんでした。 UEFI は NXE が有効になってないケースがあるようです。 でも、独自のブートローダーならカーネルの起動前にページングを操作できるのでカーネルのページ属性も自由に操作できます。

やろうと思えばここからメニューを出したりさらに高機能なブートローダーにすることもできないことはないのですが、高機能ブートローダーはいずれ別に作ることとし、カーネルを配置するだけのブートローダーはあまり前面に出てこない方がいいと思ってますヽ(•̀ω•́ )ゝ✧

Rust で ARM64-UEFI Hello World

以前書いた日記の続きになります。

neriring.hatenablog.jp

Rust ではさまざまなターゲットがあり、比較的容易にクロスコンパイルできます。 一方 UEFI も PC に依存しないように設計されていて、 x86 系とは全く別のアーキテクチャである ARM 系 CPU などにも対応しています。

つまり、 x64 用に書いた UEFI アプリは再コンパイルすればほぼそのまま ARM64(aarch64) でも動くはずです。

しかし Rust のターゲットには x64-uefi や aa64-windows があるのに aa64-uefi がありません。 Rust 自体は ARM には対応しているので、カスタムターゲットという仕組みを使えばサポートできるのでは・・・?

やってみた

まずは、 ARM64-windows のターゲット定義ファイルを取得します。

% rustc +nightly -Z unstable-options --print target-spec-json --target aarch64-pc-windows-msvc

次に、 x64-uefi のターゲット定義ファイルを取得します。

% rustc +nightly -Z unstable-options --print target-spec-json --target x86_64-unknown-uefi

これで aarch64-pc-windows-msvc.jsonx86_64-unknown-uefi.json ができました。 この2つを悪魔合体させて aarch64-unknown-uefi.json を作成します。

とりあえずビルドが通ったサンプルを置いておきます。細かい設定間違ってるかもしれないので自己責任でお願いします。

gist.github.com

完成したら、ハローワールドのフォルダにコピーして以下のようなコマンドでビルドします。

% rustup run nightly cargo xbuild --target aarch64-unknown-uefi.json --release

しばらくパソコンが頑張って、 target/aarch64-unknown-uefi/release/uefi-rs-hello.efi というファイルが出来あがれば完成です。 mnt/EFI/BOOT/BOOTAA64.EFI というファイル名に変更したら以下のようなコマンドを実行してみます。

% qemu-system-aarch64 -M virt -cpu -cortex-a57 -bios var/ovmfaa64.fd -s -drive format=raw,file=fat:rw:mnt -monitor stdio

なお、 OVMF を持ってない人は https://github.com/retrage/edk2-nightly から RELEASEAARCH64_QEMU_EFI.fd などをダウンロードして var/ovmfaa64.fd というファイルで置いておいてください。

f:id:neriring16:20200808031657p:plain

起動しました!

まとめ

UEFI アプリは再コンパイルするだけで他の CPU でもかんたんに動くことがわかりました。

ところで ARM の UEFI の将来性は?

多分これが一番無駄だと思います。

EFER.LME と EFER.LMA

EFER.LME と EFER.LMA をご存知ですか? これらのフラグは x64 CPU でロングモードの遷移に関わるフラグです。

では、2つのフラグの役割の違いをご存知ですか?

ロングモードの遷移

ロングモードの遷移についておさらいしてみましょう。 *1

まずは、 CR0 レジスタの PE フラグをセットします。 リアルモードからプロテクトモードに遷移します。

次に、 CR4 レジスタの PAE フラグをセットします。 PAE と呼ばれるページング拡張機能を有効にすることを宣言します。(しかし実際にはこの機能は使われません!)

次に、 MSR 内の EFER レジスタの LME フラグをセットします。 ロングモードを有効にすることを宣言します。しかし、この段階はまだプロテクトモードです。まだロングモードではありません。

最後に CR0 レジスタの PG フラグをセットします。 通常この操作はページングを有効にしますが、上記のフラグが全てセットされてる場合に限りロングモードに遷移します。 そしてこの時 EFER レジスタの LMA フラグが自動的にセットされます。

なお、ロングモードからプロテクトモードに戻る場合は、 CR0 レジスタの PG フラグをクリアすれば LMA フラグが自動的にクリアされてプロテクトモードに戻ることができます。

PG フラグの ON/OFF でロングモードに遷移しちゃうなら通常のページングはどうやって使えばいいのかというと、 EFER レジスタの LME フラグをクリアすれば PG フラグを再びセットした時にロングモードへの遷移が行われず通常のページングモードになります。

モード遷移するだけで何個のシステムレジスタ操作すればいいんや!!!

というのは置いておいて、以上の手順でプロテクトモードとロングモードを切り替えることができます。

さて、 LME は上記のように初期化時にシステムプログラムが書き込むわけですが、 LMA に直接書き込んでモード遷移したりできないのでしょうか?

ここで分厚いマニュアルを見てみます。

AMD の場合

f:id:neriring16:20200805235750p:plain

LME/LMA とも R/W になっているので LMA を直接書き換えることができるような感じがします。 しかし、各フラグの詳細を見てみると以下のようにあります。

f:id:neriring16:20200805235803p:plain

LMA は現在の設定値から直接書き換えると GP (一般保護例外) と明記されています。 R/W だけど勝手に書き換えはできません。

Intel の場合

f:id:neriring16:20200806000948p:plain

なんということでしょう、 LMA はリードオンリーです。 経験上書き込んでも無視されます。

けつろん

LME はロングモードを有効にするかどうか宣言するフラグ、 LMA は実際にロングモードが有効になっていることを示すフラグです。

また、 LMA は自由に書き換えることができませんが、 IntelAMD で仕様が違います。

AMD64 と Intel64 はこのように細かい違いがあります。

*1:このほかにページテーブル等の準備が必要ですが、順番が厳密に定まっているわけではないのでここでは既に準備済みとします。