第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で示した例では、タスク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に示します。
このような現象を「上限のない優先度逆転」(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に示します。
セマフォとの違い
ミューテックスとセマフォは共にタスク間の排他制御を行うためのデータ構造です。ミューテックスは優先度逆転を防ぐ機構を利用できる以外は、資源数が1のセマフォ(バイナリセマフォ)と同等の機能と言えますが、その他に以下の違いもありますので注意が必要です。
- a. ミューテックスでは、ロックしたタスク以外はロックを解除できない。
- b. ミューテックスをロックしたままタスクが終了すると、ロックが自動的に解除される。
セマフォを利用すれば排他制御を実現できますが、例えば、チームで手分けして開発を行った場合や、既存のシステムに新たなタスクを追加する場合などに、今回説明したような上限のない優先度逆転現象が発生することがあります。ミューテックスは、致命的な欠陥につながる優先度逆転現象を回避するためにとても有効な排他制御機能ですので、ぜひ活用してください。
こちらも是非
“もっと見る” RTOS編
Toppers/ASP3の使い方
SOLID-OSによる割り込みは、Toppers/ASP3がベースになっており、カーネルの管理下で割り込みの処理を行いますので、「割り込みサービスルーチン(ISR)」または「管理内割り込み」と呼んだりします。それ以外のものは、「割り込みハンドラ」もしくは「カーネル管理外割り込み」と呼びます。
組み込みソフトウェア開発にRTOSを採用する場合の「4つの心得」
リアルタイムOSを使った組み込みソフトウェア開発を行う際、覚えておいたほうがよい「4つの心得」を紹介しましょう。
組み込みOSに最適なのは?リアルタイムOSとLinuxの違い
組み込みシステムの構成を考えていく上で、どのOSを採用するかは、開発初期段階においてとても重要です。一般的には、時間的制約があるシステムの場合はリアルタイムOSが、ネットワークやファイルシステム、高度なグラフィカル表示が必要な場合はLinuxが向いていると言われています。