こんにちは、ナナです。
リアルタイムOSにおいて最も重要な機能であるマルチタスクを解説します。マルチタスクとかマルチスレッドって何なの?という方は必見です。
私は常日頃から目に見えないプログラミングが動く世界を如何に具体的なイメージとして転換するかを意識しています。マルチタスクという機能が私の目からどのように映っているかを皆さんに紹介します。
そもそもタスクが何なのかわかっていない方はこちらの記事を読んでください。
本記事では次の疑問点を解消する内容となっています。
では、マルチタスクのイメージから順に学んでいきましょう。
マルチタスクを知る前にタスクのイメージのおさらい
マルチタスクを知るためにはタスクを知る必要があります。前回の記事よりも少し掘り下げながら、改めてタスクのイメージを紹介します。
CPUが動かすプログラム
プログラムというものが動く際に、現実的な世界ではCPUと呼ばれるハードウェアが皆さんの作ったプログラムを読み込み、演算し、結果を出力します。
タスクを紹介した記事にて「タスク」とは「労働者」であると解説し、タスクがプログラムを動かしていると説明しました。
しかし、実際にプログラムを動かしているのは現実世界のCPUなわけです。では、CPUとタスクの関係とはなんなのでしょうか。
タスクはCPUのアバター
ハードウェアの視点からプログラムを見るとCPUがプログラムを動かしていますが、ソフトウェアの視点からプログラムを見るとタスクがプログラムを動かしています。
この2つは矛盾しているわけではなく、共に真実です。
タスクというのはCPUのアバター(キャラクターとして化身としたイメージ)なのです。
つまり、CPUもタスクもプログラムを動かす「労働者」という立ち位置は一緒であり、ハードウェア視点なのかソフトウェア視点なのかという捉え方が違うだけなのです。
タスクが動かすプログラム
CPUのアバターであるタスクを中心にプログラムを見ると、次のようにプログラムは動作しています。CPUがタスクに変わっただけですね。でも、イメージとしてはこちらの方が想像しやすく映ると思います。
タスクというものの理解には、まずこのイメージでタスクを捉えることです。
ITRONで動くマルチタスクのイメージ
それではようやくマルチタスクの出番です。
マルチタスクとは、マルチ(複数)なタスク(労働者)なわけです。つまり、タスクというアバターを複数人登場させることがマルチタスクとなります。
アバターはCPUに対して1人
ここで困ったことがあります。CPUというハードウェアはビュートローバーのボード上に1つしか搭載されていません。
タスクというアバターはCPUの化身ですから、ビュートローバーのシステムにおいて作成できるアバターは1人だけなのです。
仲間同士であるはずが、1つのアバターの座を巡って戦おうとしています。こんな悲しいアバター同士の争いは見たくありません。この争いを仲裁してくれるのがリアルタイムOSです。
リアルタイムOSによるマルチタスク機能とは
アバター同士の戦いを避けるためには、何とかして全員をアバターとして登場させてあげる必要があります。
ここで登場するのがリアルタイムOSの持つマルチタスク機能です。
リアルタイムOSにおけるマルチタスク機能というのは、仮想的にCPUを複数搭載しているかのように見せかけるための技術なのです。
この機能を利用すれば全てのアバターを作り出すことができます。めでたし、めでたし。
さぁ、それではイメージばかりの話をしていても皆さん退屈ですので、お待ちかねのプログラムに入っていきましょう。
ITRON上でタスクを作ってみよう
まずはタスクを作るという練習をしてみましょう。
目標はタスクの記事で実施するのが難しかった、次の2つのミッションを同時並行で行うプログラムをマルチタスクで実現してみましょう。
マルチタスクになることで、どれほど楽になるかを感じてもらいます。
静的APIによるタスク生成定義
静的APIによるタスク生成を学びます。
皆さんがすでに動かしているプログラムには実はタスクが1つ作られています。システムコンフィギュレーションファイルを開いてみてください。
system.cfg
//------------------------------------------------
// タスク定義
//------------------------------------------------
CRE_TSK(TSKID_MAIN, {TA_HLNG|TA_ACT, 0, MAIN, 1, 128, NULL});
これがタスクを生成するための静的APIであるCRE_TSKの定義例です。CRE_TSKの仕様はITRON仕様書の「4.1 タスク管理機能」に記載されているため開いてみましょう。
わからない用語がたくさん出てくると思いますが、一度は目を通しておいてください。読むことで新しい用語を知り、どんどん仕様書が読めるようになっていきます。
CRE_TSKの仕様
CRE_TSKの関数プロトタイプは次のように定義されています。
CRE_TSK(ID tskid, { ATR tskatr, VP_INT exinf, FP task, PRI itskpri, SIZE stksz, VP stk});
引数パラメータを解説しておきます。
ID | tskid | 生成するタスクのID。整数として一意のIDを割り付ける。 |
ATR | tskatr | タスク属性。高級言語を示すTA_HLNGは必ず指定。 OS起動時に自動でタスクを動作させたいならTA_ACTをOR指定する。 |
VP_INT | exinf | タスク関数の引数に渡したい情報があれば指定。なければ0でよい。 |
FP | task | タスクが実行する関数ポインタ。関数名を書く。 |
PRI | itskpri | タスク優先度。数字が小さいほど優先度が高い。 |
SIZE | stksz | スタックサイズ。とりあえず128byteを指定しておく。 |
VP | stk | スタックのメモリ番地。NULLを指定すると自動割り当て。 |
現時点ではピンとこないパラメータばかりですが、必要なものは後程解説します。
CRE_TSKによるタスク生成定義の追加
では、皆さんにタスク生成の定義を追加していただきます。
LEDのミッションを行うTASK1と、ビープ音を鳴らすミッションを行うTASK2を追加し、合計3つのタスク生成が行われるようにsystem.cfgを修正してください。
追加するタスク生成情報
- タスクIDは「TSKID_TASK1」「TSKID_TASK2」を指定する。
- タスク属性には「TA_ACT」を指定する。
- 関数ポインタは「TASK1」「TASK2」を指定する。
- タスク優先度は「2」を指定する。
- スタックサイズは「128」を指定する。
system.cfg(一部抜粋)
//------------------------------------------------
// タスク定義
//------------------------------------------------
CRE_TSK(TSKID_MAIN, {TA_HLNG|TA_ACT, 0, MAIN, 1, 128, NULL});
CRE_TSK(TSKID_TASK1, {TA_HLNG|TA_ACT, 0, TASK1, 2, 128, NULL});
CRE_TSK(TSKID_TASK2, {TA_HLNG|TA_ACT, 0, TASK2, 2, 128, NULL});
タスクID
タスクIDとして使用した「TSKID_TASK1」「TSKID_TASK2」という文字名称はコンフィギュレータが解釈し、最終的には次のようにkernel_id.hに重複しない整数型のIDとしてマクロ定義されます。
/* task ID definetion */
#define TSKID_MAIN 1
#define TSKID_TASK1 2
#define TSKID_TASK2 3
タスク属性のTA_ACT指定
この属性を付けることでタスクが生成されると同時にタスク動作が開始されます。指定しない場合はsta_tsk関数やact_tsk関数のサービスコールによりタスク動作の開始指示を行う必要があります。
関数ポインタ
関数ポインタの実体となる関数は別途皆さんが定義する必要があります。後程、関数は定義してもらいます。
タスク優先度
タスク優先度は非常に大事な情報のため別途解説を行います。もう少しお待ちを。
スタックサイズ
実はスタックとはタスク単位で独立して確保される領域で、そのサイズを決めています。
ビュートローバーのメモリ領域は合計2048Byteしかないため、今回は小さめの128Byte分のスタックをタスクに割り付けています。
必要に応じて個別にサイズを拡張するのはOKです。
タスク関数の定義
CRE_TSKにて指定した関数ポインタの実体となる関数定義が必要となります。
この紐づける関数こそが対象のタスクに対して実施させるミッションとなるプログラムです。
main.cに追加しましょう。次のプログラムをベースに処理を記述して下さい。
- MAIN関数:dly_tsk(1000)のみを行うように変更
- TASK1関数:LEDのミッションを行うように変更
- TASK2関数:ビープ音を鳴らすミッションを行うように変更
MAIN関数は特に役割がないのでdly_tskにより待機し続けてもらいます。
main.c(一部のみ抜粋)
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
while(1)
{
dly_tsk(1000);
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK2関数
//------------------------------------------------
void TASK2(VP_INT exinf)
{
while(1)
{
}
return;
}
main.c(一部のみ抜粋)
//------------------------------------------------
// 概 要: MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
while(1)
{
dly_tsk(1000);
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_ON);
Led_setLight(D_LED_KIND_GREEN, D_LED_LIGHT_OFF);
dly_tsk(1000);
Led_setLight(D_LED_KIND_ORANGE, D_LED_LIGHT_OFF);
Led_setLight(D_LED_KIND_GREEN, D_LED_LIGHT_ON);
dly_tsk(1000);
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK2関数
//------------------------------------------------
void TASK2(VP_INT exinf)
{
while(1)
{
int i;
// 400msの時間ビープ音を鳴らす
for (i=0 ; i < 2 ; i++)
{
Spk_start(E_SPK_SCALE_DO);
dly_tsk(100);
Spk_stop();
dly_tsk(100);
}
// 2000ms - 400ms を待機
dly_tsk(1600);
}
return;
}
タスク関数のプロトタイプ宣言を追加
system.cfgにおいて「TASK1」「TASK2」が関数と認識できるように関数プロトタイプ宣言をmain.hに追加しておきます。
main.h(一部抜粋)
//------------------------------------------------
// プロトタイプ宣言(Prototype declaration)
//------------------------------------------------
void MAIN(VP_INT exinf);
void Main_init(VP_INT exinf);
void TASK1(VP_INT exinf);
void TASK2(VP_INT exinf);
補足ですが、このプロトタイプ宣言はsystem.cfgの先頭でインクルードされるようになっています。
INCLUDE("\"main.h\"");
INCLUDE("\"ostimer.h\"");
通常のC言語の#includeと形が異なりますが、最終的にコンフィギュレータにより#include “main.h”に成形されます。
ビルドの実施とプログラムの転送
では、ここまで出来たらビルドして実行ファイルをビュートローバーへ転送して動かしてみてください。LEDとビープ音が狙い通りに動いているのがわかるでしょう。
シングルタスクではあれほど難しかったプログラムが、マルチタスクになったことであっという間に書けることが感じられたと思います。
マルチタスクシステムの便利さとは
改めてマルチタスクの便利さを理解しておきましょう。
シングルタスクのシステムでは労働者が1人のため、全てのミッションのタイミングを調整しながらこなす必要があります。
皆さん自身がこんなことやって、と言われたらしんどい作業になりますよね。
マルチタスクでは労働者を増やすことができます。それぞれの労働者にてミッションをこなしてもらえれば、自分のミッションのみに専念できるのがわかるでしょう。
このように時間制約の異なるミッションは別タスクにて行うとプログラミングが劇的に効率よく書けるのです。
これこそがマルチタスク機能の便利さです。
マルチタスクのプログラムにおける実際の動き
仮想CPUを作り出すことで複数のアバターを作り出す仕組みは理解できたかなと思います。
しかしですよ、実際のCPUは1個しかないわけですよ。CPUが1個しかないってことは、とある瞬間においてCPUは1つの命令しかプログラムを動かせないんです。
仮想CPUって何?なんで同時並列でプログラムが動くの?って思いませんか?
リアルタイムOSのカーネル内のプログラムを見ると、本当に仮想CPUを作り出し動かす機構がプログラミングされています。ただ、本カリキュラムはITRON入門編ですから、さすがに仮想CPUを作り出す仕組みまで解説していると皆さん逃げて行ってしまいますよね。
そこで、マルチタスクのプログラムが1個のCPUで動作するイメージだけ、この段階では伝えておきましょう。
マルチタスクとは忍法?
1つのCPUをあたかも複数のCPUがあるかのように見せかけるのがマルチタスクですね。実はマルチタスクって分身の術なんです。
分身の術により忍者アバターが3人作られました。
皆さん分身の術って知ってますか?Wikipediaに書いてあるんです。
この説明、まさしくマルチタスクにおけるCPUの動きを説明したものです。1つしかないCPUを高速で切り替えることで、あたかも複数のタスクが同時並行で動いているように見せているのです。
そのため、デバッガといったツールにより、プログラムの動きを止めると分身の術が解かれ、本物のCPUがその時点で動かしているタスクが浮かび上がります。
リアルタイムOSとはソフトウェアとして存在しており、仕組みとしてのカラクリが必ずあります。今はまだ、この仕組みまでは踏み込みませんが別の機会で紹介できればと思います。
リアルタイムOS内でマルチタスクがどのように実現されているかは、ITRONカリキュラムの続編にて解説する予定(たぶん)!乞うご期待!