タスク間の同期
第2回では、T-Kernelでの処理の単位となる「タスク」について説明しました。
タスクはそれぞれ別々に動作しますが、ときとして複数のタスクで連携して処理を実行させたいことがあります。例えば、あるタスクの処理は他のタスクの処理が完了してから実行したいとか、タスクAとタスクBの処理が両方とも完了してからタスクCの処理を実行したいとか、逆に、タスクAの処理が完了したらタスクBとタスクCを実行したいとか、開発するシステムによっていろいろな組み合わせが考えられます。
このような複数タスク間での連携処理を行うために、あるタスクの処理が終わるまで別のタスクの実行を待たせておくことを「同期」と呼びます。
T-Kernelにはさまざまな種類の同期機能が用意されていますので、必要に応じて最適な同期機能を利用することで、効率の良いプログラムを開発できるようになっています。
イベントフラグ(event flag)
先に説明したような「あるタスクの処理は他のタスクの処理が完了してから実行したい」場合などに利用できる機能としてイベントフラグがあります。イベントフラグでは、処理完了などのイベントの意味を表すフラグを、ビットパターンで表現することによってタスク間の同期を行います。
リスト1は「タスクAの処理はタスクBの処理が完了してから実行する」ようにイベントフラグを利用してプログラミングした例です(*1)。
(*1)doWorkAやdoWorkBでは、各タスク用の処理を行うものとし、必要に応じて待ちを発生することがあるものとします。各タスクではdoWorkAやdoWorkB以外にもさまざまな処理を行うと思いますが、リストでは簡単にするために省略してあります。
【リスト1:タスクAのdoWorkAはタスクBのdoWorkBが完了してから実行する】
#include <basic.h> #include <tk/tkernel.h> #include <tm/tmonitor.h> IMPORT void doWorkA( void ); IMPORT void doWorkB( void ); #define FPTN 0x00000001U ID flgid; void taskA( INT stacd, VP exinf ) { UINT flgptn; while(1){ tk_wai_flg(flgid, FPTN, TWF_ORW|TWF_CLR, &flgptn, TMO_FEVR); /* doWorkB() の処理完了を待つ */ doWorkA(); /* doWorkB() の後で行うべき処理 */ } tk_ext_tsk(); } void taskB( INT stacd, VP exinf ) { while(1){ doWorkB(); /* doWorkA() の前に行うべき処理 */ tk_set_flg(flgid, FPTN); /* doWorkB() の処理完了を通知する */ } tk_ext_tsk(); } EXPORT INT usermain( void ) /* 初期タスクから呼ばれる関数 */ { T_CFLG cflg = { NULL, TA_TFIFO|TA_WMUL, 0 }; T_CTSK ctskA = { NULL, TA_HLNG|TA_RNG0, taskA, 1, 4*1024 }; T_CTSK ctskB = { NULL, TA_HLNG|TA_RNG0, taskB, 1, 4*1024 }; ID tskIdA; /* タスクAの識別子 */ ID tskIdB; /* タスクBの識別子 */ flgid = tk_cre_flg( &cflg ); /* イベントフラグを生成 */ tskIdA = tk_cre_tsk( &ctskA ); /* タスクAを生成 */ tk_sta_tsk( tskIdA, 0 ); /* タスクAの実行を開始 */ tskIdB = tk_cre_tsk( &ctskB ); /* タスクBを生成 */ tk_sta_tsk( tskIdB, 0 ); /* タスクBの実行を開始 */ tk_slp_tsk(TMO_FEVR); /* 起床待ち状態に移行 */ return 0; }
tk_wai_flg
がイベントフラグがセットされるのを待つ機能、tk_set_flg
がイベントフラグをセットする機能です。
プログラムを動作させるとタスクAの方が先に実行を開始しますが、16行目(タスクAの中)にtk_wai_flg
を入れてあるので、ここで一旦処理を停止します。その後、タスクBが(doWorkB
の処理を完了した後で)27行目のtk_set_flg
を実行すると、タスクAがtk_wai_flg
による停止状態(T-Kernelで言う「待ち状態」)から戻り、doWorkA
を実行できるようになります。
なお、T-Kernelではイベントフラグなどを利用する場合、予めその準備をしておく必要があります。これが40行目のtk_cre_flg
(イベントフラグの生成)です。T-Kernelでは、目的に応じて複数のイベントフラグを生成して利用することができます。
リスト1と同じ様に、タスクAとタスクBの処理が両方とも完了してからタスクCの処理を実行する、タスクAの処理が完了したらタスクBとタスクCを実行するという処理もイベントフラグを利用して実現できますし、それ以外にもさまざまな同期の方法があります。
排他制御
T-KernelのようなリアルタイムOSにおいて、同期とともによく使う機能として「排他制御」があります。
タスクは見かけ上並列に実行されていますので、複数のタスクが同時に同じ資源(共有変数など)にアクセスして何らかの処理を実行しようとした場合、複数のタスクによる処理が競合することにより、正常な結果が得られない場合があります。このような現象を防止する機構が「排他制御」で、T-Kernelではセマフォと「ミューテックス(第5回参照)」を提供しています。
あわせて読みたい
例えば、あるシステムでは3つのタスクがそれぞれ別の処理を実行していて、システム全体としてはタスクが処理を完了した回数をカウントするカウンタ(変数)が1つ用意されているものとします。
この場合、各タスクは例えばリスト2のようにプログラミングすることができます。
【リスト2:タスクAのプログラム例】
void taskA( INT stacd, VP exinf ) { while(1){ doWorkA(); /* タスクA用の処理 */ counter++; /* カウントアップ */ } tk_ext_tsk(); /* タスクの終了 */ }
他のタスク(タスクBとタスクC)についても同じようなプログラムにすれば、一見すると正しく動作するように見えます。しかし、実際には以下のようなケースでうまく動作しません。
例えば、タスクBのcounter++
の部分を実行中に、タスクAが実行状態になって、タスクBの処理に割り込む形でcounter++
を実行してしまうと、カウントの処理が狂ってしまいます。
そのからくりはこうです。
counter++
は、C言語では1行で書けますが、counter
の変数がメモリに置かれている場合、CPUから見た処理手順は以下のようになります。
counter++の処理手順
- 変数(counter)から現在の値を読み出す。
- 読み出した値に1を加算する。
- 新しい値を変数(counter)に書き込む。
ちなみに、この処理をArmのアセンブリ言語で書くと、例えばリスト3のようになります。
【リスト3:Armのアセンブリ言語で書いた counter++ の例】
※ counter は別途ラベルが定義されているものとします。
ldr r2, counter // (1)-1 ldr r3, [r2, #0] // (1)-2 add r3, r3, #1 // (2) str r3, [r2, #0] // (3)
タスクBが「2」まで実行した後で、より優先度の高いタスクAが実行状態になってcounter++
を実行したとします。その場合、タスクBの処理は一旦中断され、タスクAが「1」から「3」の処理を先に実行してcounter
の値を更新します。その後、タスクBが「2」から処理を再開してcounter
の値を更新します。つまり、タスクAが書き込んだ新しいcounter
の値が、タスクBの「3」の処理によって上書きされ、タスクAによる更新結果が消えてしまったことになります。
このような現象が発生しないようにするためには、「1」から「3」の処理を連続して(他のタスクに邪魔されずに、すなわち排他的に)実行できるようにプログラミングする必要があります。このような動作は、「排他制御」の機能によって実現します。
セマフォ(Semaphore)
排他制御には、いろいろな実現方法がありますが、代表的なものがセマフォを利用する方法です。
リスト4にセマフォを使った排他制御を追加した例を示します。
【リスト4:排他制御を含めたプログラム例】
#include <basic.h> #include <tk/tkernel.h> #include <tm/tmonitor.h> IMPORT void doWorkA( void ); IMPORT void doWorkB( void ); IMPORT void doWorkC( void ); volatile INT counter = 0; ID semid; void taskA( INT stacd, VP exinf ) { while(1){ doWorkA(); /* タスクA用の処理 */ tk_wai_sem(semid,1,TMO_FEVR); counter++; /* カウントアップ */ tk_sig_sem(semid,1); } tk_ext_tsk(); } void taskB( INT stacd, VP exinf ) { while(1){ doWorkB(); /* タスクB用の処理 */ tk_wai_sem(semid,1,TMO_FEVR); counter++; /* カウントアップ */ tk_sig_sem(semid,1); } tk_ext_tsk(); } void taskC( INT stacd, VP exinf ) { while(1){ doWorkC(); /* タスクC用の処理 */ tk_wai_sem(semid,1,TMO_FEVR); counter++; /* カウントアップ */ tk_sig_sem(semid,1); } tk_ext_tsk(); } EXPORT INT usermain( void ) /* 初期タスクから呼ばれる関数 */ { T_CSEM csem = { NULL, TA_TFIFO|TA_FIRST, 1, 1 }; T_CTSK ctskA = { NULL, TA_HLNG|TA_RNG0, taskA, 1, 4*1024 }; T_CTSK ctskB = { NULL, TA_HLNG|TA_RNG0, taskB, 2, 4*1024 }; T_CTSK ctskC = { NULL, TA_HLNG|TA_RNG0, taskC, 2, 4*1024 }; ID tskIdA; /* タスクAの識別子 */ ID tskIdB; /* タスクBの識別子 */ ID tskIdC; /* タスクCの識別子 */ tk_chg_pri(TSK_SELF,1); semid = tk_cre_sem( &csem ); /* セマフォを生成 */ tskIdB = tk_cre_tsk( &ctskB ); /* タスクBを生成 */ tk_sta_tsk( tskIdB, 0 ); /* タスクBの実行を開始 */ tskIdC = tk_cre_tsk( &ctskC ); /* タスクCを生成 */ tk_sta_tsk( tskIdC, 0 ); /* タスクCの実行を開始 */ tskIdA = tk_cre_tsk( &ctskA ); /* タスクAを生成 */ tk_sta_tsk( tskIdA, 0 ); /* タスクAの実行を開始 */ tk_slp_tsk(TMO_FEVR); /* 起床待ち状態に移行 */ return 0; }
リスト4のcounter++
のように、複数のタスクで1つの変数(資源)を操作するような場合に、その部分をtk_wai_sem
とtk_sig_sem
で囲みます。セマフォは、使用されていない資源の有無や数量を数値で表現することにより、その資源を使用する際の排他制御や同期を行うための機能です。
tk_wai_sem
では、セマフォ資源を獲得しようとします。セマフォ資源を獲得できれば、セマフォ資源を減算したうえで処理を継続しますが、セマフォ資源を獲得できなければ、タスクを待ち状態に移行させます。
tk_sig_sem
では、セマフォ資源を返却します。この時、セマフォ資源獲得待ち状態のタスクがあればセマフォ資源を割り当ててそのタスクの待ち状態を解除します。
各タスクから見た場合、この時の待ち状態とは、tk_wai_sem
という関数から戻ってこないことになりますので、結果としてcounter++
の手前で実行が保留されていることになります。
さて、セマフォ資源を獲得していたタスクが、counter++
を実行した後で、tk_sig_sem
を発行するとセマフォ資源がセマフォに返却されます。すると、tk_wai_sem
を発行してセマフォ資源獲得待ち状態になっていたタスクがあればセマフォ資源を獲得してその待ち状態が解除され、tk_wai_sem
に続く処理を実行できるようになります。
かくして、先に示したcounter++
の処理手順にある「1」から「3」は、各タスク毎に連続して実行されることになり、正常にカウントされるようになりました。このようにセマフォを使うことで、共通に利用する資源(リスト4の場合はcounter
という変数)に対する排他制御を行うことが可能になります。
このプログラムであれば、tk_wai_sem
からtk_sig_sem
の間は他のタスクに邪魔されることなく実行できますので、counter++のような簡単な処理だけでなく、もっと複雑な処理も記述できます。なお、セマフォを利用する場合も予め準備をする必要があります。これがリスト4のtk_cre_sem
(57行目)です。T-Kernelではこの処理を「セマフォを生成する」と言い、目的に応じて複数のセマフォを生成して利用することができます。
こちらも是非
“もっと見る” RTOS編
Toppers/ASP3の使い方
SOLID-OSによる割り込みは、Toppers/ASP3がベースになっており、カーネルの管理下で割り込みの処理を行いますので、「割り込みサービスルーチン(ISR)」または「管理内割り込み」と呼んだりします。それ以外のものは、「割り込みハンドラ」もしくは「カーネル管理外割り込み」と呼びます。
組み込みソフトウェア開発にRTOSを採用する場合の「4つの心得」
リアルタイムOSを使った組み込みソフトウェア開発を行う際、覚えておいたほうがよい「4つの心得」を紹介しましょう。
組み込みOSに最適なのは?リアルタイムOSとLinuxの違い
組み込みシステムの構成を考えていく上で、どのOSを採用するかは、開発初期段階においてとても重要です。一般的には、時間的制約があるシステムの場合はリアルタイムOSが、ネットワークやファイルシステム、高度なグラフィカル表示が必要な場合はLinuxが向いていると言われています。