本連載では、小学生向きのプログラミング教育などに使われているBBC micro:bit(以下「micro:bit」)の上で動くようになったµT-Kernel 3.0をご紹介している。連載第4回の本号では、µT-Kernel 3.0のマルチタスクとスケジューリングの動作がわかるような例題プログラムを作成し、その動作をmicro:bit上で確認する。
µT-Kernel 3.0が動くようになったので、TRONWAREの以前の記事(*1) で紹介されていた実習用の例題を実行してみよう。スライドに書かれた実習用の例題1(図1)について、まずは頭の中でOSの動作のシミュレーションを行い、その結果を予想する。その後で実際のプログラムを作成してmicro:bit上で実行し、予想が当たっていたかを確認する。実行前にOSの動作を予想してみることで、µT-Kernel 3.0のようなリアルタイムOSのマルチタスク動作やスケジューリングのしくみに関する理解が深まる。
OSや各タスクの動作の詳細説明については以前の記事(*1) をご参照いただくとして、実行結果の予想を一つの図にまとめたものが図2である。この図では、左側から右側の水平方向が時間の経過を表しており、左端が初期状態である。各タスクは水平の線になっており、時間の経過による各タスクの状態変化を実線や点線で表現している。
図1の実習用例題からµT-Kernel 3.0のプログラムを作成する(リスト1)。なお、以下の説明文の中の(1)(2)...の括弧付き数字は、リスト1のコメントの中に入れた(1)(2)...および図2の中の(1)(2)...の括弧付き数字に対応している。また、括弧内の数字の順序が、実際の実行順序(つまり例題に対する解答)を表している。実行前のプログラムのコメントの中に実行の結果である実行順序が入っているのは、本来の手順の経過から考えると変かもしれないが、解説用のコメントということでご容赦いただきたい。
/*---------------------------------------------------------------------- * タスク遅延の例題の動作確認プログラム(μT-Kernel 3.0用) * * Copyright (C) 2022 by T3 WG of TRON Forum *----------------------------------------------------------------------*/ #include |
usermain関数の中では各タスクの初期状態を設定する。タスクAの場合は優先度10の休止状態にすればよいので、タスク生成のシステムコールtk_cre_tskを実行し(2)、その際のパラメータとして優先度10を指定する。タスクBも同様であるが(3)、優先度は20である。タスクCの場合は生成後に起動する必要があるので、優先度20を指定したtk_cre_tskを実行した後に(4)、タスク起動のシステムコールtk_sta_tskを実行する(5)。なお、エラー処理については省略している。
タスクAでは、タスク遅延のシステムコールtk_dly_tskで10ms(ミリ秒)の時間待ち(ディレイ)を行った後に(8)、自タスクを終了・削除するシステムコールtk_exd_tskで自分自身を終了、削除する(12)。タスクBも同様に、tk_dly_tskで100msの時間待ちを行った後に(10)、tk_exd_tskで自分自身を終了、削除する(16)。タスクC では、まずtk_sta_tskでタスクBを起動し(6)、次にtk_sta_tskでタスクAを起動する(7)。それからtk_dly_tskで50msの時間待ちを行った後に(9)、tk_exd_tskで自分自身を終了、削除する(14)。
このプログラムをmicro:bit上で実行して、各タスクの起動、終了・削除が行われる順序を確認し、図2のとおりで正しいという「答え合わせ」をしたい。
ところが、タスクの実行などによってµT-Kernelの内部のタスク状態が変わっても、画面などに表示が出るわけではなく、これだけではタスクの実行順序を確認できない。「タスクトレーサ」(*2) のような市販の開発ツールを使ってタスク状態などの変化を確認する方法もあるが、残念ながら、micro:bit上で使えるタスクトレーサはまだ提供されていない。そこで今回は、プログラム実行中の状態変化とその時刻のログを文字列にしてコンソール出力するようにプログラムを改造し、端末ソフトの表示を見てタスクの実行順序を確認することにしよう。ログの文字列をコンソール出力するには、T-Monitor互換ライブラリのtm_printfを使う。いわゆる「printfデバッグ」に近い方法である。
ここで一つ問題がある。tm_printfには文字列の書式変換や開発用ホストPCへのデータ転送の処理が含まれるため、µT-Kernel 3.0のシステムコールなどと比べて実行時間が長くかかる。その結果、各タスクの実行時間が大幅に延び、タスクの実行順序まで変わってしまう可能性がある。
自然科学全般において、観察する行為が観察される現象自体に変化を与えることを「観察者効果」とよぶそうだ(*3) 。この場合は観察する行為がtm_printfによるログ出力であり、観察される現象は各タスクの実行順序や状態変化のタイミングである。タスクの状態変化やシステムコール実行の際に毎回tm_printfでログ出力を行っていると、tm_printfの処理による負荷のためにタスクの実行順序まで変化するという観察者効果が発生し、観察する行為がなかった場合(ログ出力をしなかった場合)の本来のタスクの実行順序が分からなくなってしまう。
この問題を避けるには、各タスクの状態変化のログを記録する際に、その処理による負荷や実行時間を最小限にとどめる必要がある。そこで、各タスクの状態変化のときに、その場でtm_printfによるログ出力を行うのではなく、ログのメッセージの先頭アドレスとその時刻を配列変数に記録するのみとし、記録したログのコンソールへの出力は、すべてのタスクの実行が終わった後にまとめて行うようにする。
ちなみに、タスクトレーサも原理的には同じような動作をするツールであり、各タスクの状態変化時やシステムコール実行時に自動的にログを記録する。記録したログをグラフ化して分かりやすく表示する機能を加えたのがタスクトレーサである。
リスト1のプログラムにはログを記録する処理が追加されている。ログのメッセージを保持する配列変数がlog_msg[MAX_LOG]、ログの時刻を保持する配列変数がlog_time[MAX_LOG]であり、(n+1)番目のログのメッセージと時刻がlog_msg[n]とlog_time[n]に記録される(21)。log_time[n]にはミリ秒単位の時刻が記録されるが、プログラムを簡単にするために、システム時刻の下位32ビットのみを記録している。今回の用途ではこれで十分であるが、システム時刻の上位ビットが無視されるため、長時間にわたるログを記録することはできない。このほか、ログのパラメータを保持できるように、配列変数log_param[MAX_LOG]を用意している。これは、後で説明する例題2で使う。
これらの配列変数にログを記録する関数がlogであり(22)、記録されたログを最後にまとめて表示する関数がprint_logである(23)。print_logでログを表示する際には、ログが記録された実際の時刻(絶対時刻)をそのまま表示するのではなく、usermainの実行直後の最初のログの記録時刻であるlog_time[0]からの経過時間、つまり実行開始時点からの相対的な時刻(24)を表示する。プログラムの処理の詳細については、リスト1の中のコメントをご覧いただきたい。
なお、上記のような工夫をした場合でも、ログの記録による処理の負荷がゼロになったわけではない。ログを記録する関数logの中においては、現在時刻を取得するためのOSのシステムコールtk_get_timを呼び出しており、これを実行する時間はかかっている。しかし、その実行時間は例題の中に示されたディレイの時間である10msや50msと比べて十分に短いため、タスクの実行順序を変えるほどの影響はない。その結果、ログの記録による観察者効果が無視できるほど小さくなったのである。
いよいよ、リスト1のプログラムをmicro:bit上で実行する。
リスト1のソースコードは、表1のJ行で示された本誌のダウンロードページで公開している。usermain関数は、µT-Kernel 3.0のソースコード中のapp_sample¥app_main.cのファイルに含まれているので、このファイルの内容をダウンロードした内容で上書きすればよい。
具体的な操作としては、表1のJ行から開発用ホストPCにダウンロードしたファイルapp_main.cをメモ帳などのテキストエディタで開き、内容をすべて選択してコピーする。これで、リスト1の内容がWindowsのクリップボードにコピーされる。次に、Eclipseの画面でapp_main.cのソースコードを表示している部分をクリックし、[Ctrl]+[A]キーで元の内容をすべて選択してから[Delete]キーで削除する。最後に、[Ctrl]+[V]キーでクリップボードにあったリスト1の内容を貼り込む。
名 称 | 説 明 | URL | |
---|---|---|---|
J | 例題1:タスク遅延のプログラム |
本号のリスト1で示した例題プログラムのソースコード | https://www.personal-media.co.jp/book/tw/tw_index/364.html |
K | 例題2:メッセージバッファのプログラム app_main.c |
本号のリスト3で示した例題プログラムのソースコード | https://www.personal-media.co.jp/book/tw/tw_index/364.html |
あるいは、Eclipseから参照しているソースコード中のファイルapp_main.cを、ダウンロードしたファイルで直接置き換えてもよい。この場合は、app_main.cの親のフォルダにあたる C:¥mtk3¥mbit¥mtkernel_3¥app_sampleのフォルダを開き、その中にあるオリジナルのapp_main.cを削除し、代わりにダウンロードしたリスト1のファイルapp_main.cを入れる。すると、Eclipseの画面に表示されているapp_main.cのソースコードもリスト1の内容に置き換わっているはずだ。
プログラムの実行の手順は、連載第3回で実行したときと同じである。「Run」の「Run Configurations...」を選択してから画面右下の「Run」をクリックすると、修正のあったapp_main.cの保存と再コンパイル、µT-Kernel 3.0の再ビルドとmicro:bitへの転送、プログラムの実行が自動的に行われ、実行終了後には記録されたログがコンソールに出力される(リスト2)。その結果を見てタスクの実行順序を確認し、図2の予想と照合する。最初にタスクC→タスクA→タスクBの順に各タスクが起動され、その後は10ms以上の時間待ち(ディレイ)を挟みながら、タスクA→タスクC→タスクBの順に各タスクが終了して削除されることが確認できたはずだ(*4) 。
microT-Kernel Version 3.00 1: 0ms: Start User-main program 2: 0ms: Task C: start 3: 0ms: Task A: start 4: 0ms: Task B: start 5: 20ms: Task A: exit and delete 6: 60ms: Task C: exit and delete 7: 110ms: Task B: exit and delete 8: 1010ms: End User-main program |
実習用例題をもう一つ実行してみよう。こんどは、メッセージバッファを使った通信と、送信側および受信側のタスクの優先度、それらのタスクの実行順序の関係について、例題のプログラムを実行しながら確認する。この例題もTRONWAREの以前の記事注5)で紹介されたものだが、今回の記事のスタイルに合わせて、内容やプログラムを少しアレンジしている。例題2の内容を図3に、その動作を図4に示す。
図3の実習用例題から作成したプログラムをリスト3に示す。最初の例題1と同様に、実行順序を確認するためのログを記録する。ログの記録や表示を行う関数はリスト1と同じなので、リスト3では省略した。
usermain関数の中では、各タスクの生成や起動を行うほか、メッセージバッファも生成する。タスクAは高優先度、タスクBは低優先度なので、例題1と同じく、タスクAは10、タスクBは20のタスク優先度とした。
タスクAでは、for文による5回の繰り返しの中で、メッセージバッファから受信するシステムコールtk_rcv_mbfを実行する。また、tk_rcv_mbfの実行の前後にはlog関数を使ってログを記録する。メッセージ受信後のログでは、受信したメッセージがわかるように、メッセージ内の最初のデータであるmsg[0]をログのパラメータに含めている。
一方のタスクBでは、for文による5回の繰り返しの中で、MSGLENバイトのサイズを持つメッセージを作成し、システムコールtk_snd_mbfを使ってメッセージバッファに送信する。メッセージの作成時には、繰り返しのカウンタ変数であるiをメッセージ内容に含めることにより、毎回異なったメッセージを送るようにしている。こちらも、tk_snd_mbfの実行の前後にはlog関数を使ってログを記録し、送信メッセージ内の最初のデータであるmsg[0]をログのパラメータに含めている。
/*---------------------------------------------------------------------- * メッセージバッファの例題の動作確認プログラム(μT-Kernel 3.0用) * * Copyright (C) 2022 by T3 WG of TRON Forum *----------------------------------------------------------------------*/ #include |
リスト3のプログラムをmicro:bit上で実行する。実行の手順は先ほどの例題1と同じであり、Eclipseから参照しているファイルapp_main.cをリスト3のソースコードで置き換えた後に、「Run Configurations...」の画面右下の「Run」をクリックすればよい。なお、リスト3のソースコードは、表1のK行で示されたページで公開している。
実行後のログ(リスト4)を確認してみよう。タスクAは、起動した直後にtk_rcv_mbfによるメッセージバッファの受信待ちとなるため(リスト4の3行目)、その後に優先度の低いタスクBが動き出す(4行目)。タスクBは最初のメッセージ(msg[0]=0)を生成し、それをメッセージバッファに送信する(5行目)。そのメッセージをタスクAが受信するが、タスクAのほうがタスクBよりも優先度が高いため、タスクAが実行を再開する(6行目)。タスクAは、次のメッセージを受信しようとして再度メッセージバッファの受信待ちとなり(7行目)、優先度の低いタスクBが再度動き出す(8行目)。これを5回繰り返すと、タスクAが終了して削除され(23行目)、続いてタスクBも終了して削除される(25行目)。
ここまでの所要時間は0msのままである。表示される時間の分解能は10ms単位なので、タスクBの終了と削除の時点(25行目)では、まだ10ms未満の時間しか経過していないことがわかる。
microT-Kernel Version 3.00 1: 0ms: Start User-main program 2: 0ms: Task A: start 3: 0ms: Task A: before tk_rcv_mbf 4: 0ms: Task B: start 5: 0ms: Task B: before tk_snd_mbf, msg[0]=0 6: 0ms: Task A: after tk_rcv_mbf, msg[0]=0 7: 0ms: Task A: before tk_rcv_mbf 8: 0ms: Task B: after tk_snd_mbf 9: 0ms: Task B: before tk_snd_mbf, msg[0]=1 10: 0ms: Task A: after tk_rcv_mbf, msg[0]=1 11: 0ms: Task A: before tk_rcv_mbf (...中略...) 20: 0ms: Task B: after tk_snd_mbf 21: 0ms: Task B: before tk_snd_mbf, msg[0]=4 22: 0ms: Task A: after tk_rcv_mbf, msg[0]=4 23: 0ms: Task A: exit and delete 24: 0ms: Task B: after tk_snd_mbf 25: 0ms: Task B: exit and delete 26: 1010ms: End User-main program |
リスト3の例題では、メッセージを送信する側であるタスクBの優先度が低く、メッセージを受信する側であるタスクAの優先度が高かった。この関係を逆にすると、メッセージの送受信やタスクの実行順序はどのように変化するだろうか。実際にプログラムを動かして確認してみよう。
タスクAとタスクBを生成する際のタスク優先度は、app_main.cの中でT_CTSK型の構造体として定義されたctskA、ctskBの中の4番目のメンバとして指定されている。このうち、タスクAの優先度であるctskAの中の10を21に変更すると(図5)、タスクAの優先度がタスクBよりも低くなる。この状態で再度プログラムを実行し、実行後のログ(リスト5)を確認する。
mmicroT-Kernel Version 3.00 1: 0ms: Start User-main program 2: 0ms: Task B: start 3: 0ms: Task B: before tk_snd_mbf, msg[0]=0 4: 0ms: Task B: after tk_snd_mbf 5: 0ms: Task B: before tk_snd_mbf, msg[0]=1 (...中略...) 11: 0ms: Task B: before tk_snd_mbf, msg[0]=4 12: 0ms: Task B: after tk_snd_mbf 13: 0ms: Task B: exit and delete 14: 0ms: Task A: start 15: 0ms: Task A: before tk_rcv_mbf 16: 0ms: Task A: after tk_rcv_mbf, msg[0]=0 17: 0ms: Task A: before tk_rcv_mbf 18: 0ms: Task A: after tk_rcv_mbf, msg[0]=1 (...中略...) 24: 0ms: Task A: after tk_rcv_mbf, msg[0]=4 25: 0ms: Task A: exit and delete 26: 1010ms: End User-main program |
タスク優先度の変更により、メッセージを送信するタスクBのほうが、メッセージを受信するタスクAの優先度よりも高くなった。こうなると、タスクBの送信したメッセージがタスクAに受信される前に、優先度の高いタスクBが実行を継続する。すなわち、タスクAの実行が止まったままの状態で、タスクBはメッセージを続けて5回送信し(リスト5の3行目から12行目)、送信後には終了して削除される(13行目)。タスクBが終了すると、こんどはタスクAが実行を開始する(14行目)。メッセージバッファには5個のメッセージが溜まっていたので、メッセージを続けて5回受信し(15行目から24行目)、受信後に終了して削除される(25行目)。メッセージの送信側のタスクの優先度が高いと、メッセージを受信するタスクが動かないため、送信と受信が交互にならず、メッセージバッファにメッセージが滞留するのである。
* * *
今回はµT-Kernel 3.0のマルチタスクとスケジューリングの動作を確認するために、実習用の例題を元にしたプログラムを作成し、micro:bitの上で実行した。今回試した例題プログラムは非常に簡単なものであり、OSの持つタスク管理やメッセージバッファの機能のごく一部しか使っていない。しかし、µT-Kernel 3.0はこれ以外にも多くの機能を持っており、そのすべてをmicro:bitの上で実行することができる。リアルタイムOSやµT-Kernel 3.0について興味のある読者は、µT-Kernel 3.0の仕様書(*6) や参考書(*7) を見ながら、より高度な機能を使ったプログラムにも挑戦してほしい。
次回はmicro:bit上の周辺デバイスの操作を試す予定だ。