メッセージの送受信による同期
第3回では、タスク間での同期として「イベントフラグ」、「セマフォ」について説明しました。
イベントフラグおよびセマフォは、複数タスクで連携して処理を実行する場合や、排他制御する場合には非常に便利な機能です。しかし、タスク間の同期だけでなく、同時に情報を通知したいことがあります。例えば、あるタスクで処理した結果を基に別のタスクで処理したいとか、共有メモリを介さずにタスク間でデータ(T-Kernelでは「メッセージ」と呼びます)の送受信をしたいとか、開発するシステムによっていろいろな組み合わせが考えられます。
このような複数タスク間でデータを送受信することを「通信」と呼びます。また、同時に「同期」も行うために、「同期・通信」と呼ばれます。T-Kernelには以降で説明するメッセージバッファやメールボックス機能などが用意されていますので、必要に応じて最適な同期機能を利用することで、効率の良いプログラムを開発できるようになっています。
メッセージバッファ(message buffer)
T-Kernelでは、メッセージの送受信ができる機能を複数用意していますが、その一つとしてメッセージバッファがあります。メッセージバッファは、可変長のメッセージを送受信することのできる同期・通信機能の一つです。
送信側はメッセージを送受信のためのメッセージバッファ領域に格納し、受信する側はメッセージバッファ領域からメッセージを一つ取り出すことで同期・通信を実現しています。
メッセージバッファにメッセージが格納されていない場合にメッセージを受信しようとすると、受信待ち状態になります。一方で、メッセージバッファ領域に十分な空き領域がない場合にメッセージを送信しようとすると、送信待ち状態になります。
リスト1は「タスクAの処理は、タスクBで実行する処理 doWorkB()の結果を受け取り、その結果を doWorkA()で利用する」ようにメッセージバッファを利用してプログラミングした例です(*1)。
(*1)doWorkAやdoWorkBでは、各タスク用の処理を行うものとし、必要に応じて待ちを発生することがあるものとします。各タスクではdoWorkAやdoWorkB以外にもさまざまな処理を行うと思いますが、リストでは簡単にするために省略してあります。
【リスト1:メッセージバッファを利用してプログラミングした例】
#include <basic.h> #include <tk/tkernel.h> #include <tm/tmonitor.h> IMPORT void doWorkA( W ); IMPORT W doWorkB( void ); ID mbfid; void taskA( INT stacd, VP exinf ) { INT msgsz; W rcv_msg; while(1){ msgsz = tk_rcv_mbf( mbfid, &rcv_msg, TMO_FEVR ); /* doWorkB() の処理完了を待ち、その処理結果を受信する */ if( msgsz > 0 ) { doWorkA( rcv_msg ); /* 受信した結果を利用 */ } } tk_ext_tsk(); } void taskB( INT stacd, VP exinf ) { W snd_msg; while(1){ snd_msg = doWorkB(); /* doWorkA() の前に行うべき処理 */ tk_snd_mbf( mbfid, &snd_msg, sizeof(snd_msg), TMO_FEVR ); /* doWorkB() の処理結果をタスクAに通知する */ } tk_ext_tsk(); } EXPORT INT usermain( void ) /* 初期タスクから呼ばれる関数 */ { T_CMBF cmbf = { NULL, TA_TFIFO, 0x10, 0x4 }; T_CTSK ctskA = { NULL, TA_HLNG|TA_RNG0, taskA, 1, 4*1024 }; T_CTSK ctskB = { NULL, TA_HLNG|TA_RNG0, taskB, 2, 4*1024 }; ID tskIdA; /* タスクAの識別子 */ ID tskIdB; /* タスクBの識別子 */ mbfid = tk_cre_mbf( &cmbf ); /* メッセージバッファを生成 */ 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_rcv_mbf
がメッセージを受信する機能、tk_snd_mbf
がメッセージを送信する機能です。
プログラムを動作させるとタスクAの方が先に実行を開始しますが、17行目(タスクAの中)にtk_rcv_mbf
を入れてあるので、メッセージ受信待ちになり、ここで一旦処理を停止します。その後、タスクBが実行状態となります。タスクBが(doWorkB
の処理を完了した後で)32行目のtk_snd_mbf
を実行すると、タスク優先度高(=1)のタスクAがtk_rcv_mbf
による停止状態(T-Kernelで言う「待ち状態」)から戻り、doWorkA
を実行できるようになります。
なお、T-Kernelではイベントフラグなどと同様に、予め利用するメッセージバッファの準備をしておく必要があります。これが46行目のtk_cre_mbf
(メッセージバッファの生成)です。T-Kernelでは、目的に応じて複数のメッセージバッファを生成して利用することができます。
ここで示した同期方法は一例にすぎません。例えば、メッセージバッファ領域のサイズが0のメッセージバッファを生成すると、tk_snd_mbf
でメッセージ送信待ち状態にすることも可能です。この場合、メッセージは送信側タスクが用意したバッファから、受信側タスクが用意したバッファへと直接コピーされます。
メッセージバッファを利用すると、ここで示した以外にも、さまざまな依存関係に合わせた同期・通信の機能が実現できます。
メールボックス(Mailbox)
メッセージを送受信できる機能は複数用意されていますが、メッセージバッファの他にメールボックスもあります。
リスト2にメールボックスを利用してプログラミングした例を示します(*1)。
(*1)doWorkAやdoWorkBでは、各タスク用の処理を行うものとし、必要に応じて待ちを発生することがあるものとします。各タスクではdoWorkAやdoWorkB以外にもさまざまな処理を行うと思いますが、リストでは簡単にするために省略してあります。
【リスト2:メールボックスを利用してプログラミングした例】
#include <basic.h> #include <tk/tkernel.h> #include <tm/tmonitor.h> IMPORT void doWorkA( UH ); IMPORT UH doWorkB( void ); typedef struct { T_MSG msg; UH data; } UserMSG; ID mbxid; ID tskIdA; /* タスクAの識別子 */ ID tskIdB; /* タスクBの識別子 */ void taskA( INT stacd, VP exinf ) { ER ercd; UserMSG rcv_umsg; while(1){ ercd = tk_rcv_mbx( mbxid, (T_MSG **)&rcv_umsg, TMO_POL ); if( ercd == E_TMOUT ) { tk_wup_tsk( tskIdB ); } else { doWorkA( rcv_umgs.data ); /* タスクA用の処理 */ } } tk_ext_tsk(); } void taskB( INT stacd, VP exinf ) { UserMSG umsg[5]; INT cnt = 0; while(1){ umsg.data[cnt] = doWorkB(); /* タスクB用の処理 */ tk_snd_mbx( mbxid, (T_MSG *)&umsg[cnt] ); if( cnt != 4 ) { cnt++; } else { tk_slp_tsk( TMO_FEVR ); cnt = 0; } } tk_ext_tsk(); } EXPORT INT usermain( void ) /* 初期タスクから呼ばれる関数 */ { T_CMBX cmbx = { NULL, TA_TFIFO|TA_MFIFO }; T_CTSK ctskA = { NULL, TA_HLNG|TA_RNG0, taskA, 2, 4*1024 }; T_CTSK ctskB = { NULL, TA_HLNG|TA_RNG0, taskB, 1, 4*1024 }; mbxid = tk_cre_mbx( &cmbx ); /* メールボックスを生成 */ tskIdB = tk_cre_tsk( &ctskB ); /* タスクBを生成 */ tk_sta_tsk( tskIdB, 0 ); /* タスクBの実行を開始 */ tskIdA = tk_cre_tsk( &ctskA ); /* タスクAを生成 */ tk_sta_tsk( tskIdA, 0 ); /* タスクAの実行を開始 */ tk_slp_tsk(TMO_FEVR); /* 起床待ち状態に移行 */ return 0; }
tk_rcv_mbx
がメッセージを受信する機能、tk_snd_mbx
がメッセージを送信する機能です。
プログラムを動作させるとタスクBの方が先に実行を開始します。タスクBの中の40行目でメッセージを作成し、42行目のtk_snd_mbx
で送信しています。メッセージを5回送った段階で起床待ち状態で待ちになり、ここで一旦処理を停止します。その後、タスクAが実行状態となります。タスクAが23行目のtk_rcv_mbx
を実行すると、タスクBから送られてきたメッセージを受信することができます。送信されたメッセージが無くなった段階で、26行目でタスクBに起床要求します。タスクBはtk_slp_tsk
による停止状態(T-Kernelで言う「待ち状態」)から戻り、再びメッセージを送信し始めます。
メールボックスもメッセージバッファと同様に、予めその準備をしておく必要があります。これが60行目のtk_cre_mbx
(メールボックスの生成)です。T-Kernelでは、目的に応じて複数のメールボックスを生成して利用することができます。
メールボックスがメッセージバッファより優れている点は、メッセージバッファ領域を必要としないため、必要最小限のメモリ領域を動的に確保するだけで実装できる点です。メッセージバッファと異なり、メールボックスではメッセージのコピーを行うのではなく、メッセージの先頭アドレスを通知しています。このため、メッセージのサイズが大きい場合は、メールボックスよりも高速に処理できるといった利点があります。ただし、送信した後もそのメッセージを受信側が受信して処理を終えるまでは、メッセージを改変・削除してはいけません。処理が終わる前に、送信メッセージを改変すると、意図しない動作になる可能性があるため、注意が必要です。
通常は、送信側で「メモリプール機能(第8回参照)」を利用してメモリ領域を確保し、そこにメッセージを作成してからメールボックスに送信します。受信側では受信したメッセージを使い終わったらメモリプールに返却します。このように実装することで、受信側がメッセージを使い終わる前にメッセージを改変してしまうといった誤りを防ぐことができます。
あわせて読みたい
こちらも是非
“もっと見る” RTOS編
Toppers/ASP3の使い方
SOLID-OSによる割り込みは、Toppers/ASP3がベースになっており、カーネルの管理下で割り込みの処理を行いますので、「割り込みサービスルーチン(ISR)」または「管理内割り込み」と呼んだりします。それ以外のものは、「割り込みハンドラ」もしくは「カーネル管理外割り込み」と呼びます。
組み込みソフトウェア開発にRTOSを採用する場合の「4つの心得」
リアルタイムOSを使った組み込みソフトウェア開発を行う際、覚えておいたほうがよい「4つの心得」を紹介しましょう。
組み込みOSに最適なのは?リアルタイムOSとLinuxの違い
組み込みシステムの構成を考えていく上で、どのOSを採用するかは、開発初期段階においてとても重要です。一般的には、時間的制約があるシステムの場合はリアルタイムOSが、ネットワークやファイルシステム、高度なグラフィカル表示が必要な場合はLinuxが向いていると言われています。