イベントフラグ、セマフォ

タスク間の同期

第2回では、T-Kernelでの処理の単位となる「タスク」について説明しました。

タスクはそれぞれ別々に動作しますが、ときとして複数のタスクで連携して処理を実行させたいことがあります。例えば、あるタスクの処理は他のタスクの処理が完了してから実行したいとか、タスクAとタスクBの処理が両方とも完了してからタスクCの処理を実行したいとか、逆に、タスクAの処理が完了したらタスクBとタスクCを実行したいとか、開発するシステムによっていろいろな組み合わせが考えられます。

このような複数タスク間での連携処理を行うために、あるタスクの処理が終わるまで別のタスクの実行を待たせておくことを「同期」と呼びます。

T-Kernelにはさまざまな種類の同期機能が用意されていますので、必要に応じて最適な同期機能を利用することで、効率の良いプログラムを開発できるようになっています。

図1

【タスク間の連携に使う同期機能】

イベントフラグ(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

【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++の処理手順

  • (1)変数(counter)から現在の値を読み出す。
  • (2)読み出した値に1を加算する。
  • (3)新しい値を変数(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ではこの処理を「セマフォを生成する」と言い、目的に応じて複数のセマフォを生成して利用することができます。