この先の開発

このドキュメントで扱ったのは、スケジューリングの初歩の初歩まででです。 すでに世の中に出回っているOSと比べると機能はまだまだお粗末なものです。 また、実装の仕方もあまり洗練されたものとは言えないでしょう。 では、この先よりOSらしいものを実装するとなるとどういう機能を実装していけばいいのか。 そのヒントとなるトピックをいくつか取り上げてみます。

リアルタイムスケジューリング

組込みアプリケーションで大事なことのひとつとして「リアルタイム性」というものがあります。 これは決められた処理を一定の時間内に行う、という性質です。 このリアルタイム性にはスケジューリングが重要な役割を果たします。 通常のデスクトップOSでは細かいデッドラインが各アプリケーションに対して決まっている、というわけではなく、 複数のアプリケーションが均等に実行できるようなことが重視され(もちろん優先度を決めたりすることでリアルタイム性を担保する仕組みもありますが)、 組込みシステムではこのリアルタイム性がより重視されることが多く、スケジューリングのアルゴリズムもデスクトップのものとは異なってきます。

いくつかリアルタイム性を担保する上での課題を紹介していきます。

まず、プロセスにこれは最優先で処理すべきものと、そうでもないもの、のような差が存在します。 例えば、歩行ロボットがバランスを取るために駆動部を動かすための計算をして指示を出す、というプロセスはきちんと決まった時間内で実行しないと転倒してしまうことになりますが、 目を光らせるとか、人と対話をするといったためのプロセスは多少の遅れがあっても深刻な影響はでないであろう、といった感じです。 この歩行ロボットの例ではプロセスに固定された優先度を付与しておき、優先度が高い用のプロセスのリストと低い用のリストを持っておき、前者のリストが空になってから後者のリストからプロセスをスケジュールする、といった方法を取れば解決できそうですね。

ロボットの例は静的に優先度が決定できる、というものですが、実行状況や時刻によってこの優先度が動的に変動する場合もあります。 リアルタイムシステムにはタイマーなどにより定期的に実行されるプロセスと、ボタンを押したあとに発生するなど外部からのイベントによって不定期に実行する必要があるプロセス、というものが存在します。 つまり、プロセスのスタートするタイミングや締め切りは変動しうるということです。 そのことを踏まえ例えば、締切が3分後だが実行には2分かかるプロセスAと締切が10分後だが実行には7分かかるプロセスBが同時に発生した場合、後者のプロセスBから実行してしまうとAの締切に間に合いません。 このような場合、締切が近いプロセスAから実行することによってすべてのプロセスが締め切りに間に合うようにしてあげる必要があります。 しかし、このプロセスAがプロセスBの締め切り2分前で実行が残り1分かかる、という段階で発生した場合、まずプロセスBを終わらせてからプロセスAを実行してもプロセスAの締切には間に合うので このような場合はプロセスBから実行しなければなりません。

プロセス間で資源を取り合っていると問題はもっと複雑になることもあります。 例えばネットワーク通信が必要な締め切りが厳しいプロセスAと、ネットワーク通信を使うが締め切りを守らなくてもいいプロセスB、ネットワーク通信は使わないが締め切りが厳しいプロセスCが存在するとします。 プロセスBが実行してネットワーク通信をおこなっているときにプロセスCを実行する必要が出てきたとします。 この場合、ネットワーク通信を一時中断してプロセスCを実行しなければなりません。ここでプロセスAを実行しなければならなくなり、しかもプロセスCよりも締め切りが近いので最優先に実行したいとします。 プロセスAはネットワーク通信をしたいのですが、そのためにはまず、中断されている通信を終了させる必要があります。そうすると、本来優先度が低いはずのプロセスCから実行しなければならない、ということになってしまいます。 そのため、このようにデバイスなどの共有資源を持っている場合、共有先のプロセスの優先度を継承して実行させる、といった仕組みを持つ場合もあります。

このように、リアルタイム性という性質を満たすにはこのドキュメントで実装したラウンドロビン型のスケジューラでは解決できない課題が山のようにあるわけです。 また、これらの課題を解決するために長い時間をかけて計算するわけにはいかないので、OS内で実装するアルゴリズムの計算量にも気をつかう必要もあります。

デバイスドライバ

このドキュメントではほとんど使いませんでしたが、本来はマイコンボードについているペリフェラルを使ってシステムを構築します。 例えば、マイコンボードでのプログラミングの代名詞であるLチカにはLEDを動かすためのGPIOというモジュールを操作しなければなりませんが、 OSはこれらのペリフェラルへの簡単なプログラミングインターフェースを提供することも期待されています。

これらペリフェラルのためのデバイスドライバを書くにあたって、参考文献でも紹介していますが、Rust EmbeddedワーキンググループのDiscovery Bookなどのドキュメントは参考になるかもしれません。 これはRust Embeddedグループの提供するクレートやツールを用いてRustの性質を活かしたデバイスへのアクセスの仕方を解説しています。

ヒープアロケータ

ベアメタルプログラミングではmallocのようなプログラム実行時に動的にメモリを確保することができず、スタック領域や静的に確保した領域を利用してプログラミングする必要があります。 動的なメモリ確保は本来OSの仕事だからです。そのため、自作OSにおいても動的メモリ確保のための機能を実装するのは大事なトピックです。 この動的メモリ確保用のメモリ領域をヒープと呼び、ここからメモリ領域を確保したり使い終わったメモリをヒープに戻すプログラムをヒープアロケーターといいます。

ヒープアロケーターの実装方法はいろいろありますが、代表的なアロケーターの実装を参考文献でものせたWriting an OS in Rustで紹介されています。 実はRustはこのヒープアロケータを言語標準のものでなく自前で実装したものに置き換えるための仕組みが用意されています。 詳しくはRustの公式ドキュメントのGlobalAllocを参考にしてください。 この方法に従いヒープアロケータを実装しておくことで、stdで使える一部の構造体が含まれるallocクレートも利用可能になります。

OS上で動かすアプリケーション以外でもOSそのものの中でヒープアロケーションの機能を利用することは当然できますが、 動的なメモリ確保は失敗する可能性があり、またアルゴリズムによっては実行時間が長くなる危険性もあることには注意しましょう。

テスト

TRPLのテストの章で紹介されているように、Rustには自動テストのための機能が標準で備わっています。 しかしながら、少し触れたようにこの仕組みをno_std環境でそのまま用いることはできません。

テストを書く方法はいくつか存在します。1つはクレートとして一部のモジュールを別プロジェクトに切り出してしまうことです。 このドキュメントの例で言うと連結リストモジュールは別クレートとして切り出すことができるので、これに対してテストを書くということができます。 しかし、このOSプロジェクトそのものにテストを書くのもnightlyのcustom_test_frameworks機能を使えば可能です。 x86系のアーキテクチャをベースとした解説ですが、Writing an OS in RustのTestingの章で詳しく解説されています。 Cortex-MのようなアーキテクチャでもQEMUを用いたエミュレーションができるので、同様の方針である程度テストを書くことができるでしょう。

外部クレートの活用

自作OSというと、様々なコンポーネントを自分の手で書いていく、というのが醍醐味の1つでもあります。 しかしながら、Rustにはcargoを用いて外部のライブラリを簡単に導入できる仕組みがあるので、これを利用して既存のコードに依存してしまうのも1つの手だと筆者は考えています。 もちろん、実際のOSだと信頼性の担保が難しいということで、外部のコンポーネントの導入に消極的であったりという事情はありますが、趣味の範囲であればそこまで深く考える必要はないでしょう。

ただし、実際の多くのクレートはstdの存在を仮定するのもが多く、導入する際は"No Standard Library"のカテゴリがついているかに注意が必要です。 このドキュメントでも利用したcortex-m-semihostingやEmbeddedグループの公開しているデバイスドライバなどのクレート、ヒープアロケータを提供するクレートなどは組み込んでみるとできることが一気に増えるでしょう。 その他にもいくつか自作OSの作成に役に立つクレートを紹介します。

  • heapless:ヒープ領域を必要としない、いくつかの著名なデータ構造を提供するためのライブラリ
  • volatile-register:メモリ領域をvolatileとしてアクセスするためのインターフェースを提供するライブラリです。デバイスドライバを自作する際に重宝するでしょう
  • tock-register:Tockという組込みOSでの成果物でもある、MMIO領域のより強力な抽象化を提供してくれるライブラリ
  • aligned:アラインメントが保証されたデータタイプを提供するライブラリ