借り初めのひみつきち

仮ブログです。

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:大規模なコンピュータではタイムシェアリングなどありましたが