第2回では、Schedulerについて説明します。Schedulerは、第1回の「擬人化」「擬似パソコン化」の説明の中で、最後に登場したソフトウェア部品です。
Schedulerは、ソフトウェア部品が有するグローバル関数の内、並列実行の対象となる関数群のスケジューリング(実行順序制御)を担います。
グローバル関数が呼ばれるタイミング
各ソフトウェア部品は、それぞれ幾つかのグローバル関数を有しますが、それらのグローバル関数が呼ばれるタイミングは、以下の3種類に分類できます。
- 他のソフトウェア部品から直接呼ばれる。
他のソフトウェア部品に対して、入出力、表示、生成等の機能を提供するグローバル関数は、その機能が必要になったタイミングでそれを必要とする他のソフトウェア部品から直接、呼ばれます。 - Event Generatorのコールバック関数として呼ばれる。
ハードウェアの入出力、変換等、状態が変化したタイミングでEvent Generatorから呼ばれます。 - 並列処理関数として呼ばれる。
他のソフトウェア部品と並列に実行しなければならない関数は、Schedulerと呼ばれるソフトウェア部品から呼ぶようにします。
この記事では、並列処理関数の設計手法である「ステートマシン」について説明した後、上記3のSchedulerによるスケジューリングについて説明します。
なお本記事では、リアルタイムOSなしでスケジューリングを行う事を想定しています。
並列処理を実現するための留意点
組み込みシステムでは、前記1~2のいずれの場合でも、1つの関数でCPU実行権を長時間、占有する事は許されません。なぜなら、そういう関数を作ってしまうと、並列動作させるべき他の処理が実行できないからです。
1回あたりの関数呼び出しで許容されるCPU占有時間(すなわち実行時間)は、開発するシステムの要件に依存します。故に、一概に「XXmsec以内」とは言えませんが、たとえばイベント発生待ちや、大量の計算/検索のために、実行時間の長い繰り返し処理を1回の関数呼び出しで処理する事は避けた方が良いと思います。
では、どうすれば良いでしょうか? この記事では、並列処理関数の設計手法として「ステートマシン」を紹介します。
ステートマシンとは
ステートマシンは、組み込みソフトウェアでは一般的な設計手法です。オブジェクト指向設計のデザインパターンでは「ステートパターン」に相当します。関数が呼ばれた際、毎回、同じ処理を実行するのでは無く、現在の「状態」に応じて異なる処理を実行するように設計します。
たとえばある処理を行う過程で、イベント発生を待たなければならないとします。この場合、関数の中の while文でイベント発生を待ってしまうと、目的のイベントが発生するまで、CPU実行権を占有してしまいます。この問題を回避するため、たとえば「前処理」「イベント発生待ち」「メイン処理」の3つのグローバル関数に分割し、Schedulerに呼び分けてもらう案も考えられますが、この方法では、ソフトウェア部品の内部仕様を外部にさらす事になり、更にグローバル関数の数も増えるため、望ましくありません。
こういうケースでは、ステートマシンの設計手法を利用します。イベント待ちが必要ならば「初期状態」「イベント待ち状態」の2つの状態を持つようにします。この「状態」を保持する変数は、enum型のstatic変数として定義します。
typedef enum {
e_Init = 0,
e_WaitEvent
} EN_State;
static EN_State lv_state = e_Init; /* 関数の状態種別 */
関数の実装では、swtich~case文で、現在の状態に応じて、異なる処理を実行するようにします。
void 並列処理_その1(void)
{
switch(lv_state) {
case e_Init:
前処理();
lv_state = e_WaitEvent;
break;
case e_WaitEvent:
if (イベント発生チェック() == true) { /* イベント発生済かチェック */
メイン処理(); /* イベントに応じたメイン処理 */
lv_state = e_Init; /* 次回の呼び出しに備えて、状態を戻す */
}
else {
; /* イベント未発生なら何もせずに直ぐに戻る */
}
break;
defalut:
内部エラー処理(); /* あり得ない(内部エラー処理) */
lv_state = e_Init;
break;
}
}
イベント発生は、上記の関数呼び出しとは非同期に、Event Generatorによってコールバック関数で通知してもらうようにしておき、その情報は、static変数に保存しておきます。イベント発生チェック()では、コールバック関数で通知され、static変数に保存された直近のイベント情報をチェックするようにします。
このように設計すれば、このソフトウェア部品と外部のソフトウェア部品間のインタフェースは、イベント通知のコールバック関数と、並列処理関数の2つに集約でき、更にこのソフトウェア部品がどういう状態を持つかは外部から隠ぺいできます。
Schedulerによるスケジューリング
前記のとおり、並列処理関数をステートマシンで設計した後、Schedulerと呼ばれるソフトウェア部品から、各並列処理関数を呼び出します。
その際の最も単純なスケジューリングの方法は、ラウンドロビンと呼ばれる方法です。ラウンドロビンでは、並列処理に相当するグローバル関数を単純に順番に、繰り返し呼び出します。たとえば、以下のような感じです。
void Scheduler_main(void)
{
while (true) {
並列処理_その1();
並列処理_その2();
並列処理_その3();
並列処理_その4();
}
}
上記関数は、関数mainから呼ばれる無限ループの関数です。これが最も単純なスケジューリングの方法です。
この方法に対して更に、イベントAが発生した場合は「並列処理_その1()」と「並列処理_その2()」は休止させて、代わりに「並列処理_その5()」を実行し、その処理が終わり次第、元の状態に戻すというようなスケジューリングを追加する事も可能です。この場合、Scheduler内のスケジューリング関数Scheduler_main自体をステートマシン化し、以下のように設計します。
typedef enum {
e_Normal = 0,
e_Emergency
} EN_State;
static EN_State lv_state = e_Normal; /* 関数の状態種別 */
void Scheduler_main(void)
{
while (true) {
bool isComplete = false;
/* イベント発生チェック */
if (lv_state == e_Normal) {
if (イベントAの発生チェック() == true) {
lv_state = e_Emergency;
}
}
swtich (lv_state) {
case e_Normal:
並列処理_その1(); /* 定常時の処理 */
並列処理_その2();
break;
case e_Emergency:
isComplete = 並列処理_その5(); /* 緊急時の処理 */
if (isComplete == true) {
lv_state = e_Normal; /* 緊急処理が完了したなら状態を戻す */
}
break;
}
並列処理_その3(); /* 定常時/緊急時 共に実行すべき処理 */
並列処理_その4();
}
}
上記の実装例では、イベントAの発生後、緊急処理の「並列処理_その5()」が開始されるまでの待ち時間は、最大で「並列処理_その1()」~「並列処理_その4()」の最大時間の「合計」になります。この待ち時間が長すぎる場合は、関数Scheduler_mainの状態管理を詳細化する事で、「並列処理_その1()」~「並列処理_その4()」の中の「最大時間」まで短縮できます。
以上のように、ステートマシンとSchedulerの設計手法を利用する事で、リアルタイムOSなしでも、ソフトウェア部品の独立性を維持しつつ、リアルタイム性を確保した設計が可能になります。
第2回はここまでです。次回からは、本記事で提案する以下の2つの習慣
- 「特定の役割り」を担う「部品」によってソフトウェア全体を構成する。
- オブジェクト指向プログラミングの習慣を取り入れる。
のうち、後者の「オブジェクト指向プログラミングの習慣を取り入れる」について説明します。