ミューテックス

ミューテックス

第3回では共有資源を使用する際にタスク間で排他制御を行うための機能としてセマフォを紹介しましたが、T-Kernelではセマフォだけでなくミューテックスも提供しています。

ミューテックスはセマフォと同様にタスク間の排他制御を行うための機能を提供しますが、排他制御に伴って発生する上限のない優先度逆転を防ぐ機構をサポートします。

排他制御に伴う一時的な実行順序の逆転

T-Kernelでは各タスクに優先度が与えられており、優先度の高いタスクが優先的に実行されます。このようにして次に実行すべきタスクを決定する処理を優先度ベースのスケジューリングと呼びます。

優先度ベースのスケジューリングでは常に優先度の高いタスクが優先的に実行されますが、タスク間で共有資源の排他制御を行うと、優先度の高いタスクが一時的に待ち状態になるため、タスクの優先度とタスクの実行順序が一致しない場合が生じます。

リスト1に高優先度タスクと低優先度タスクの実行順序が逆転するプログラム例を示します。

高優先度のタスクAは10秒に1度、5秒かかる処理を実行します。処理後は5秒間起床待ち状態に遷移します。低優先度のタスクCは5秒かかる処理を起床待ち状態に遷移することなく繰り返し実行します。タスクAとタスクCは資源(リスト1ではループ変数)を共有しているので排他制御を行う必要があります(*1)。これをセマフォを使って実現します。

  • (*1)リスト1でループ変数を共有していることに特別な意味はありません。何らかの資源を共有する場合、排他制御を行わなければ正しく動作しないことを分りやすく示すために例としてこのように実装しています。

【リスト1】

								#include <basic.h>
								#include <tk/tkernel.h>
								#include <tm/tmonitor.h>

								IMPORT	void	doWorkA( void );
								IMPORT	void	doWorkC( void );

										INT		i;						/* タスクAとタスクCで共有			*/
										ID		semid;					/* セマフォID					*/

								void	taskA( INT stacd, VP exinf )
								{
										while(1){
												tm_putstring( (UB*)"A enters critical section!¥n" );
												tk_wai_sem(semid, 1, TMO_FEVR);

												doWorkA();				/* タスクA用の処理				 */

												tm_putstring( (UB*)"A exits critical section!¥n" );
												tk_sig_sem(semid, 1);

												tk_dly_tsk(5000);
										}

										tk_ext_tsk();
								}

								void	taskC( INT stacd, VP exinf )
								{
										while(1){
												tm_putstring( (UB*)"C enters critical section!¥n" );
												tk_wai_sem(semid, 1, TMO_FEVR);

												doWorkC();				/* タスクC用の処理				 */

												tm_putstring( (UB*)"C exits critical section!¥n" );
												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	ctskC = { NULL, TA_HLNG|TA_RNG0, taskC, 3, 4*1024 };
										ID		tskIdA;					/* タスクAの識別子				 */
										ID		tskIdC;					/* タスクCの識別子				 */

										tk_chg_pri(TSK_SELF,1);

										semid = tk_cre_sem( &csem );	/* セマフォを生成				 */

										tskIdA = tk_cre_tsk( &ctskA );	/* タスクAを生成   			*/
										tk_sta_tsk( tskIdA, 0 );		/* タスクAの実行を開始			*/

										tskIdC = tk_cre_tsk( &ctskC );	/* タスクCを生成   			*/
										tk_sta_tsk( tskIdC, 0 );		/* タスクCの実行を開始			*/

										tk_slp_tsk(TMO_FEVR);			/* 起床待ち状態に移行			*/
										return 0;
								}

								void	doWorkA( void )
								{
										for (i = 0; i < 50; i++) {
												tm_putstring( (UB*)"A is working!¥n" );
												WaitUsec(100000);		/* 100 ms のビジーループ(*2)	 */
										}
								}

								void	doWorkC( void )
								{
										for (i = 0; i < 50; i++) {
												tm_putstring( (UB*)"C is working!¥n" );
												WaitUsec(100000);		/* 100 ms のビジーループ		 */
										}
								}
								
  • (*2)WaitUsec()は、指定された時間分(マイクロ秒)の微小待ちを行うAPIです。短時間の待ちを行う場合などに利用します。この待ちは通常はビジーループで実装されますので、タスクは実行状態のままになります。本稿に示したリスト1〜3では「(カーネルの待ちを含まない)一定時間の処理」であることを表わすために利用しています。

図1は、タスクCがタスクAより先にセマフォを獲得したことによって、タスクAがタスクCに待たされ、実行の順序が逆転する際のタスク状態の変化を示しています。

図1

【図1:タスクの実行順序が逆転する例】

図1で示した例では、タスクAとタスクCの実行順序の逆転状態は、タスクCがセマフォを解放するまで続きます。これは、排他制御に伴う待ち状態の発生により一時的に生じた逆転状態であり、設計者の意図の範囲ですので、特に問題はありません。

上限のない優先度逆転

リスト1で示したプログラムに中優先度のタスクBが追加されたプログラムをリスト2に示します。

タスクBは28秒に1度、23秒かかる処理を実行します。処理後は5秒間起床待ち状態に遷移します。ただし、タスクBはタスクAとタスクCが共有する資源を使用していませんので、排他制御は行っていません。

【リスト2】

								#include <basic.h>
								#include <tk/tkernel.h>
								#include <tm/tmonitor.h>

								IMPORT	void	doWorkA( void );
								IMPORT	void	doWorkB( void );
								IMPORT	void	doWorkC( void );

										INT		i;						/* タスクAとタスクCで共有		 */
										ID		semid;					/* セマフォID				 */

								void	taskA( INT stacd, VP exinf )
								{
										while(1){
												tm_putstring( (UB*)"A enters critical section!¥n" );
												tk_wai_sem(semid, 1, TMO_FEVR);

												doWorkA();				/* タスクA用の処理				*/

												tm_putstring( (UB*)"A exits critical section!¥n" );
												tk_sig_sem(semid, 1);

												tk_dly_tsk(5000);
										}

										tk_ext_tsk();
								}

								void	taskB( INT stacd, VP exinf )
								{
										while(1){
												doWorkB();				/* タスクB用の処理				*/

												tk_dly_tsk(5000);
										}

										tk_ext_tsk();
								}

								void	taskC( INT stacd, VP exinf )
								{
										while(1){
												tm_putstring( (UB*)"C enters critical section!¥n" );
												tk_wai_sem(semid, 1, TMO_FEVR);

												doWorkC();				/* タスクC用の処理				*/

												tm_putstring( (UB*)"C exits critical section!¥n" );
												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, 3, 4*1024 };
										ID		tskIdA;					/* タスクAの識別子				*/
										ID		tskIdB;					/* タスクBの識別子				*/
										ID		tskIdC;					/* タスクCの識別子				*/

										tk_chg_pri(TSK_SELF,1);

										semid = tk_cre_sem( &csem );	/* セマフォを生成			  */

										tskIdA = tk_cre_tsk( &ctskA );	/* タスクAを生成				*/
										tk_sta_tsk( tskIdA, 0 );		/* タスクAの実行を開始		 */

										tskIdB = tk_cre_tsk( &ctskB );	/* タスクBを生成				*/
										tk_sta_tsk( tskIdB, 0 );		/* タスクBの実行を開始		 */

										tskIdC = tk_cre_tsk( &ctskC );	/* タスクCを生成				*/
										tk_sta_tsk( tskIdC, 0 );		/* タスクCの実行を開始		 */

										tk_slp_tsk(TMO_FEVR);			/* 起床待ち状態に移行		 */
										return 0;
								}

								void	doWorkA( void )
								{
										for (i = 0; i < 50; i++) {
												tm_putstring( (UB*)"A is working!¥n" );
												WaitUsec(100000);		/* 100 ms のビジーループ		*/
										}
								}

								void	doWorkB( void )
								{	
										INT	j;

										for (j = 0; j < 230; j++) {
												tm_putstring( (UB*)"B is working!¥n" );
												WaitUsec(100000);		/* 100 ms のビジーループ		*/
										}
								}

								void	doWorkC( void )
								{
										for (i = 0; i < 50; i++) {
												tm_putstring( (UB*)"C is working!¥n" );
												WaitUsec(100000);		/* 100 ms のビジーループ		*/
										}
								}
								

図1で示したタスク実行例と同様にタスクCがタスクAより先にセマフォを獲得すると、タスクAはタスクCがセマフォを解放するまで待ち状態になります。この状態でタスクBが処理を開始すると、タスクBは低優先度のタスクCを実行状態から実行可能状態にするので、タスクCは処理を停止し、タスクCのセマフォ解放を待っているタスクAもタスクBの処理が完了するまで待たされることになります。この時のタスク状態の変化を図2に示します。

図2

【図2:上限のない優先度逆転の例】

このような現象を「上限のない優先度逆転」(unbounded priority inversion)と呼びます。

リスト2では一定時間でタスクBの処理が完了するように実装してありますが、より複雑なプログラムであった場合、タスクBによる待ちがいつ完了するか明確でない場合もあり、システムとして深刻な状態に陥ることがあります。

例えば、1997年に火星に着陸したマーズ・パスファインダー号の制御プログラムでも上限のない優先度逆転が発生し、システムが再起動に陥るという事態が発生したというのは有名な話です。

ミューテックスの利用

リスト3はリスト2と同じタスクから構成されるプログラムですが、タスクAとタスクCの排他制御をミューテックスを使って実現しています。

【リスト3】

									#include <basic.h>
									#include <tk/tkernel.h>
									#include <tm/tmonitor.h>

									IMPORT	void	doWorkA( void );
									IMPORT	void	doWorkB( void );
									IMPORT	void	doWorkC( void );

											INT		i;						/* タスクAとタスクCで共有		 */
											ID		mtxid;					/* ミューテックスID			*/

									void	taskA( INT stacd, VP exinf )
									{
											while(1){
													tm_putstring( (UB*)"A enters critical section!¥n" );
													tk_loc_mtx(mtxid, TMO_FEVR);

													doWorkA();				/* タスクA用の処理				*/

													tm_putstring( (UB*)"A exits critical section!¥n" );
													tk_unl_mtx(mtxid);

													tk_dly_tsk(5000);
											}

											tk_ext_tsk();
									}

									void	taskB( INT stacd, VP exinf )
									{
											while(1){
													doWorkB();				/* タスクB用の処理				*/

													tk_dly_tsk(5000);
											}

											tk_ext_tsk();
									}

									void	taskC( INT stacd, VP exinf )
									{
											while(1){
													tm_putstring( (UB*)"C enters critical section!¥n" );
													tk_loc_mtx(mtxid, TMO_FEVR);

													doWorkC();				/* タスクC用の処理				*/

													tm_putstring( (UB*)"C exits critical section!¥n" );
													tk_unl_mtx(mtxid);
											}

											tk_ext_tsk();
									}

									EXPORT	INT	usermain( void )			/* 初期タスクから呼ばれる関数	*/
									{
											T_CMTX	cmtx  = { NULL, TA_INHERIT, 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, 3, 4*1024 };
											ID		tskIdA;					/* タスクAの識別子				*/
											ID		tskIdB;					/* タスクBの識別子				*/
											ID		tskIdC;					/* タスクCの識別子				*/

											tk_chg_pri(TSK_SELF,1);

											mtxid = tk_cre_mtx( &cmtx );	/* ミューテックスを生成		 */

											tskIdA = tk_cre_tsk( &ctskA );	/* タスクAを生成				*/
											tk_sta_tsk( tskIdA, 0 );		/* タスクAの実行を開始		 */

											tskIdB = tk_cre_tsk( &ctskB );	/* タスクBを生成				*/
											tk_sta_tsk( tskIdB, 0 );		/* タスクBの実行を開始		 */

											tskIdC = tk_cre_tsk( &ctskC );	/* タスクCを生成				*/
											tk_sta_tsk( tskIdC, 0 );		/* タスクCの実行を開始		 */

											tk_slp_tsk(TMO_FEVR);			/* 起床待ち状態に移行		 */
											return 0;
									}

									void	doWorkA( void )
									{
											for (i = 0; i < 50; i++) {
													tm_putstring( (UB*)"A is working!¥n" );
													WaitUsec(100000);		/* 100 ms のビジーループ		*/
											}
									}

									void	doWorkB( void )
									{	
											INT	j;

											for (j = 0; j < 230; j++) {
													tm_putstring( (UB*)"B is working!¥n" );
													WaitUsec(100000);		/* 100 ms のビジーループ		*/
											}
									}

									void	doWorkC( void )
									{
											for (i = 0; i < 50; i++) {
													tm_putstring( (UB*)"C is working!¥n" );
													WaitUsec(100000);		/* 100 ms のビジーループ		*/
											}
									}
									

ミューテックスを利用した場合、タスクAがミューテックスのロックを試みた時点で、タスクCの優先度がタスクAの優先度と同じ値まで引き上げられますので、タスクCはタスクBよりも優先的に処理が実行されることになります。これにより、タスクCが(ミューテックスをロックしている間は)タスクBよりも優先的に実行されることになり、結果的にタスクAがタスクBに待たされる状態になることを防ぐことができます。

この時のタスク状態の変化を図3に示します。

図3

【図3:ミューテックスを利用した場合の動作】

セマフォとの違い

ミューテックスとセマフォは共にタスク間の排他制御を行うためのデータ構造です。ミューテックスは優先度逆転を防ぐ機構を利用できる以外は、資源数が1のセマフォ(バイナリセマフォ)と同等の機能と言えますが、その他に以下の違いもありますので注意が必要です。

  • a. ミューテックスでは、ロックしたタスク以外はロックを解除できない。
  • b. ミューテックスをロックしたままタスクが終了すると、ロックが自動的に解除される。

セマフォを利用すれば排他制御を実現できますが、例えば、チームで手分けして開発を行った場合や、既存のシステムに新たなタスクを追加する場合などに、今回説明したような上限のない優先度逆転現象が発生することがあります。ミューテックスは、致命的な欠陥につながる優先度逆転現象を回避するためにとても有効な排他制御機能ですので、ぜひ活用してください。