サンプルプロジェクトのビルド、実行からデバッグまでができるようになりました。
ターミナルには、以下のようなメッセージが表示されているはずです。
今ターゲット上で動いているのは、TOPPERS/ASPの標準的なサンプルプログラムです。
これを操作して、TOPPERS/ASPが準拠する「μITRON」の動きを確認してみましょう。
サンプルプログラムのコンフィグレーション
まず、Eclipse上で「sample1.cfg」というファイルを開いてください。
これはコンフィグレーションファイルといって、このアプリケーションで使用されるOSのリソース(タスク、セマフォ、イベントフラグや割り込みなど)を予めここに記述しておくファイルです。
このファイルはビルド時にMakefileの中から呼ばれる「cfg.exe」というアプリケーションによって解析され、これらのリソースを生成するためのソースコードを自動的に生成してくれます。
生成されるファイルは「kernel_cfg.c」や「kernel_cfg.h」という名前で、ビルドの途中で突然現れます。
この仕組によって、アプリケーションで上で特にコーディングで作成することなく、OSのリソースにアクセスすることができるようになります。
注目していただきたいのは、「sample1.cfg」の中程の以下の記述です。
- CRE_TSK(TASK1, { TA_NULL, 1, task, MID_PRIORITY, STACK_SIZE, NULL });
- CRE_TSK(TASK2, { TA_NULL, 2, task, MID_PRIORITY, STACK_SIZE, NULL });
- CRE_TSK(TASK3, { TA_NULL, 3, task, MID_PRIORITY, STACK_SIZE, NULL });
- CRE_TSK(MAIN_TASK, { TA_ACT, 0, main_task, MAIN_PRIORITY, STACK_SIZE, NULL });
1行目を翻訳すると…「STACK_SIZE」で示されるスタック領域を持ち、「MID_PRIORITY」で示される優先度を持ち、「1」の引数を入れた「task()」という関数が実体の「TASK1」というIDのタスクを「TA_NULL(特に設定なし)」で作成してください…という意味になります。
STACK_SIZEやMID_PRIORITYの実際の値は「sample1.h」に記述されています。
MID_PRIORITYで指定するタスクの優先度は、値が小さいほど優先度は高くなります。
このTASK1は、実行された時に「sample1.c」に実装された「task()」関数に引数「1」が入った状態でスタートします。
但し、TA_NULL(特に設定なし)で設定されているため、タスクは作成されてはいるものの、OSの起動時には待機中となり、すぐには「task()」関数に飛ぶことはありません。
2および3行目は、1行目の内容とほぼ同様で、単にIDに付随する番号と、タスクの実体である「task()」という関数に入れる引数が異なるだけです。
問題は4行目。
これを翻訳しますと…「STACK_SIZE」で示されるスタック領域を持ち、「MAIN_PRIORITY」で示される優先度を持ち、「0」の引数を入れた「main_task()」という関数が実体の「MAIN_TASK」というIDのタスクを「TA_ACT(OS起動後即実行)」で作成してください…という意味になります。
タスクの優先度を設定するMAIN_PRIORITYという値は、TASK1~3で指定したMID_PRIORITYよりも小さな値が設定されているはずです。
そして、今回はTA_ACT(OS起動後即実行)というフラグが与えられています。
プログラムをターゲットに転送して実行すると、即座にタスクの実体である「main_task()」(「sample1.c」に実装されています)に処理が移ります。
このページ(TOPPERS/ASPのビルドからデバッグまで~サンプルプロジェクトのデバッグ)の最後の方で、既にそれを確認しています。
以上のように、このサンプルプログラムは4つのタスクが作成され、その内の一つのタスク(MAIN_TASK)が一番高い優先度でOSの起動時に最初に実行されるというコンフィグレーション(設定)となっていることが分かります。
MAIN_TASKの動作
コンフィグレーションにより、OSが起動すると同時にMAIN_TASKが走り出します。
すなわち「sample1.c」の「main_task()」関数から処理がスタートします。
さながら、普通のC言語で言うところの「main()」関数と同じような感覚です。
では、せっかく作った他のタスクTASK1~3はどうしているのか?
前述の通り、OSの起動時点では待機状態です。
しかし、これらはプログラム実行後、程なくしてMAIN_TASKから起動されます。
「sample1.c」の「main_task()」関数の中盤の以下のコードに注目してください。
- /*
- * タスクの起動
- */
- SVC_PERROR(act_tsk(TASK1));
- SVC_PERROR(act_tsk(TASK2));
- SVC_PERROR(act_tsk(TASK3));
重要なのは「act_tsk()」という関数です。
(「SVC_PERROR」マクロについては、ただのエラー処理ですので無視してください。)
これは、引数として指定したIDに該当するタスクを起動せよ、という「μITRON」のサービスコールです。
TOPPERS/ASPに限らず、「μITRON」に準拠するRTOSなら、同じサービスコールを持っており、動作もほぼ同様のはずです。
(そうでなければ、「μITRON」準拠を名乗れません。)
引数として指定するIDは、必ずコンフィグレーションファイルで定義されたものでなければなりません。
(定義は「kernel_cfg.h」に記述されています。)
このサンプルプログラムの場合は、ここでTASK1~3の全てを起動しています。
タスクを起動したからといって、この瞬間に処理がTASK1~3の実体である「task()」関数に飛ぶわけではありません。
なぜならば、今動作しているMAIN_TASKタスクはTASK1~3よりも優先度が高いからです。
一連の「act_tsk()」の後も、MAIN_TASKタスクはそのまま処理を続行します。
とはいえ、それも長くは続きません。
すぐ下の「メインループ」とコメントされたコードに注目してください。
- /*
- * メインループ
- */
- do {
- SVC_PERROR(serial_rea_dat(TASK_PORTID, &c, 1));
- switch (c) {
- case 'e':
- case 's':
- case 'S':
- case 'd':
- case 'y':
- case 'Y':
- case 'z':
- case 'Z':
- message[tskno-1] = c;
- break;
- …
5行目の「serial_rea_dat()」という関数の部分ですが、これはシリアル通信で送られてくる1文字の受信を待つ、という処理になっています。
例えば、TeraTermなどのターミナルにキーボードから1文字を送られると、待ち状態から開放されてMAIN_TASKは再び起動し、受け取った1文字をその下のswitch文により文字の種類に応じた処理をするようにプログラムされています。
そして、それが終わったら再び1文字を待つ、ということを永遠に繰り返しています。
もし、いつまでも1文字が届かなければ、MAIN_TASKはここでずっと待機となります。
MAIN_TASKが待ち状態になると、次に優先度が高いタスクに処理が移ります。
今回の場合は、TASK1の実体である「task()」関数に「1」の引数が付加されて実行されます。
このように、タスクの処理が移り替わることを「ディスパッチ」と呼びます。
では、なぜTASK1なのでしょうか?
コンフィグレーションでは、TASK1~3は全て同じMID_PRIORITYの優先度が設定されているのでTASK2やTASK3が実行されても良いように思えます。
これは、同じ優先度を持つタスクが待機中となってディスパッチが発生した場合、コンフィグレーションで生成された順番で実行されるという仕様によるものです。
たとえば、TASK2だけMID_PRIORITYよりも高い優先度(小さい値)が設定されていた場合は、ディスパッチの際に有無も言わさずTASK2が起動しますが、同等である場合は、先に生成された順、具体的にはIDの値が低い方(「kernel_cfg.h」を参照)が優先されることになります。
このように、これ以降のMAIN_TASKは、単純にターミナルからの1文字を待っており、それが来ないうちはずっと待機しているだけのヒマなタスクとなっています。
TASK1~3の動作
さて、MAIN_TASKが待ち状態に入ったことによりディスパッチが発生し、処理はTASK1、すなわち「1」の引数が入った「task()」関数に移りました。
このとき、ターミナルには以下のような表示が現れているはずです。
TASK1は定期的に自分が起動していることと、そのカウントを出力しています。
この部分のソースコードは以下のとおりです。
- /*
- * 並行実行されるタスク
- */
- void task(intptr_t exinf)
- {
- volatile ulong_t i;
- int_t n = 0;
- int_t tskno = (int_t) exinf;
- const char *graph[] = { "|", " +", " *" };
- char c;
- SVC_PERROR(ena_tex());
- while (true) {
- syslog(LOG_NOTICE, "task%d is running (%03d). %s",
- tskno, ++n, graph[tskno-1]);
- for (i = 0; i < task_loop; i++);
- c = message[tskno-1];
- message[tskno-1] = 0;
- switch (c) {
- case 'e':
- syslog(LOG_INFO, "#%d#ext_tsk()", tskno);
- SVC_PERROR(ext_tsk());
- assert(0);
- case 's':
- syslog(LOG_INFO, "#%d#slp_tsk()", tskno);
- SVC_PERROR(slp_tsk());
- break;
- …
whileループの中、14行目の「syslog()」関数に注目してください。
これは、指定された文字列をシリアル送信する関数です。
引数の文字列を見ると、ターミナルで表示されている内容と一致しますね。
次に16行目の単純ループです。
ターゲットのCPUのスピードによって異なりますが、一般的に「task_loop」変数にはとても大きい値が入っていて、それを空ループさせることによって時間待ちをしています。
17行目に出てくる「message」配列変数はグローバル変数であり、タスクIDをインデックスとして、MAIN_TASKで受信された1文字を格納しています。
ここは、それを取り出して「c」変数に格納している処理となります。
それ以降は、その「c」変数、すなわちMAIN_TASKで受信された1文字の種類に応じた処理を以下のswitch文で行っています。
これを空ループで稼いだ時間毎に延々と繰り返す処理を行っています。
構成としてはMAIN_TASKと似ていますね。
ここで注目しなければならないのは、ターミナルからの受信が何もない場合、つまり「syslog()」関数によるメッセージ表示だけを行っている限り、この「task()」関数はディスパッチを一切行わないことです。
だから、ターミナルを確認すると、いつまで経ってもTASK1だけが動作しています。
例えば、今このサンプルプログラムがWindowsなどの汎用OSの上で動作している場合は、どこかのタイミングで、同時に起動しているはずのTASK2やTASK3などの他のタスクに処理が切り替わってもおかしくはありません。
一般的に汎用OSは、決められた短い時間単位で複数のタスク(プロセス/スレッド)を高速に切り替える(ディスパッチする)ことによって1つのCPUでも複数のプログラムを並列に処理しているように見せる、いわゆるマルチタスクを実現しています。
このような動作を「タイムスライス」や「タイムクォンタム」と呼びます。
この常識が「μITRON」のようなRTOS(リアルタイムOS)では通用しません。
RTOSでは、プログラム上でディスパッチの機会をプログラマーが作ってあげない限り、タスクの切り替えが起こりません。
たとえば、上記のソースコードの15行目の単純ループですが、時間待ち目的で「μITRON」の「dly_tsk()」や「tslp_tsk()」などのサービスコールを使った場合を考えてみましょう。
これらのサービスコールはディスパッチを伴います。
したがって、この場合には時間待ちの間にTASK2やTASK3に処理が切り替わります。
今回のサンプルプログラムでは、それでは目的と反するので、わざとディスパッチを伴わない空ループを使っているのです。
このディスパッチのタイミングの違いは、特にパソコン上で動くアプリケーションソフトのプログラマーがRTOSを使い始めた時に一番引っかかるところですので、ご注意ください。
この違いは優劣の問題ではなく、OSの設計思想の違いといえます。
まんべんなく全てのプロセスやスレッドをバランス良く同時実行したい場合は汎用OSを、とにかく必要な処理を状況に応じて最低限のリソースでクリティカルに実行したい場合はRTOSを…といった棲み分けがあるのです。
サンプルプログラムの操作
それでは、実際にサンプルプログラムで動作確認をしていきましょう。
まずは散々出てきている以下の画像。
前述の通り、TASK1だけが動作している状態です。
今、TASK1~3の3つのタスクは、全て同じMID_PRIORITYという優先度で起動していますが、CPUを専有して動いているのはTASK1のみという状態です。
ここから、TASK2に切り替えてみましょう。
任意のタスクを起動させるために一番分かり易いのは、そのタスクの優先度を上げることです。
すなわち、TASK2をMID_PRIORITYよりも優先度の高いHIGH_PRIORITYに設定し直しましょう。
これを行うには、まず命令対象(今回はTASK2)のタスクの番号「2」と、優先度を上げるためのコマンド文字「>」を続けてターミナルに対して入力します。
以下のような表示となり、TASK1に変わって、今度はTASK2が動作を続ける状態となりました。
TASK1は、TASK2の方が優先度が高くなったので処理を明け渡したのでしょう。
次に、今動いているTASK2を10秒間だけ待機中にしてみましょう。
これを行うには、まず命令対象(今回はTASK2)のタスクの番号「2」と、10秒間タスクを休止するためのコマンド文字「d」を続けてターミナルに対して入力します。
すると、以下のようにTASK2は動作を停止し、TASK1が再び動作を始めました。
優先度が高かったTASK2が待機中となり、その次に優先度が高いTASK1に処理が移るようにディスパッチした模様です。
コマンド文字「d」による待機は10秒間だけですので、10秒経過すると自動的にTASK1が動作を停止し、TASK2が再び動作を始めます。
では、優先度を高く(HIGH_PRIORITY)に設定したTASK2を元の普通の優先度(MID_PRIORITY)に戻してみましょう。
これを行うには、まず命令対象(今回はTASK2)のタスクの番号「2」と、優先度を普通に戻すコマンド文字「=」を続けてターミナルに対して入力します。
すると、以下のようにTASK2が動作を停止し、TASK1が動作を始めました。
タスクの優先度を変更する「chg_pri()」関数は、ディスパッチを伴うサービスコールです。
実行するとディスパッチが発生します。
この瞬間、MAIN_TASKを除く全てのタスクがMID_PRIORITYで同等なので、「同じ優先度を持つタスクが待機中となってディスパッチが発生した場合、コンフィグレーションで生成された順番で実行される」という仕様に添ってTASK1が動作するようにOSが選んだのでしょう。
コマンド一覧
このサンプルプログラムには、他にもOSの機能を確認するための多くの有益なコマンドがあります。
詳しくは、「sample1.c」の冒頭のコメントを参考にしてください。
- /*
- * サンプルプログラム(1)の本体
- *
- * ASPカーネルの基本的な動作を確認するためのサンプルプログラム.
- *
- * プログラムの概要:
- *
- * ユーザインタフェースを受け持つメインタスク(タスクID: MAIN_TASK,優
- * 先度: MAIN_PRIORITY)と,3つの並行実行されるタスク(タスクID:
- * TASK1〜TASK3,初期優先度: MID_PRIORITY)で構成される.また,起動周
- * 期が2秒の周期ハンドラ(周期ハンドラID: CYCHDR1)を用いる.
- *
- * 並行実行されるタスクは,task_loop回空ループを実行する度に,タスクが
- * 実行中であることをあらわすメッセージを表示する.空ループを実行する
- * のは,空ループなしでメッセージを出力すると,多量のメッセージが出力
- * され,プログラムの動作が確認しずらくなるためである.また,低速なシ
- * リアルポートを用いてメッセージを出力する場合に,すべてのメッセージ
- * が出力できるように,メッセージの量を制限するという理由もある.
- *
- * 周期ハンドラは,三つの優先度(HIGH_PRIORITY,MID_PRIORITY,
- * LOW_PRIORITY)のレディキューを回転させる.プログラムの起動直後は,
- * 周期ハンドラは停止状態になっている.
- *
- * メインタスクは,シリアルI/Oポートからの文字入力を行い(文字入力を
- * 待っている間は,並行実行されるタスクが実行されている),入力された
- * 文字に対応した処理を実行する.入力された文字と処理の関係は次の通り.
- * Control-Cまたは'Q'が入力されると,プログラムを終了する.
- *
- * '1' : 対象タスクをTASK1に切り換える(初期設定).
- * '2' : 対象タスクをTASK2に切り換える.
- * '3' : 対象タスクをTASK3に切り換える.
- * 'a' : 対象タスクをact_tskにより起動する.
- * 'A' : 対象タスクに対する起動要求をcan_actによりキャンセルする.
- * 'e' : 対象タスクにext_tskを呼び出させ,終了させる.
- * 't' : 対象タスクをter_tskにより強制終了する.
- * '>' : 対象タスクの優先度をHIGH_PRIORITYにする.
- * '=' : 対象タスクの優先度をMID_PRIORITYにする.
- * '<' : 対象タスクの優先度をLOW_PRIORITYにする.
- * 'G' : 対象タスクの優先度をget_priで読み出す.
- * 's' : 対象タスクにslp_tskを呼び出させ,起床待ちにさせる.
- * 'S' : 対象タスクにtslp_tsk(10秒)を呼び出させ,起床待ちにさせる.
- * 'w' : 対象タスクをwup_tskにより起床する.
- * 'W' : 対象タスクに対する起床要求をcan_wupによりキャンセルする.
- * 'l' : 対象タスクをrel_waiにより強制的に待ち解除にする.
- * 'u' : 対象タスクをsus_tskにより強制待ち状態にする.
- * 'm' : 対象タスクの強制待ち状態をrsm_tskにより解除する.
- * 'd' : 対象タスクにdly_tsk(10秒)を呼び出させ,時間経過待ちにさせる.
- * 'x' : 対象タスクに例外パターン0x0001の例外処理を要求する.
- * 'X' : 対象タスクに例外パターン0x0002の例外処理を要求する.
- * 'y' : 対象タスクにdis_texを呼び出させ,タスク例外を禁止する.
- * 'Y' : 対象タスクにena_texを呼び出させ,タスク例外を許可する.
- * 'r' : 3つの優先度(HIGH_PRIORITY,MID_PRIORITY,LOW_PRIORITY)のレ
- * ディキューを回転させる.
- * 'c' : 周期ハンドラを動作開始させる.
- * 'C' : 周期ハンドラを動作停止させる.
- * 'b' : アラームハンドラを5秒後に起動するよう動作開始させる.
- * 'B' : アラームハンドラを動作停止させる.
- * 'z' : 対象タスクにCPU例外を発生させる(タスクを終了させる).
- * 'Z' : 対象タスクにCPUロック状態でCPU例外を発生させる(プログラムを
- * 終了する).
- * 'V' : get_utmで性能評価用システム時刻を2回読む.
- * 'v' : 発行したシステムコールを表示する(デフォルト).
- * 'q' : 発行したシステムコールを表示しない.
- */
- …
是非、色々と遊んでみてください。
0 件のコメント:
コメントを投稿