タスク例外

タスク例外処理機能

今回は、T-Kernelのタスク例外処理機能について説明します。

本連載の第2回から第5回にかけて、T-Kernelのタスク管理機能とタスク間での同期・通信に関する機能(イベントフラグセマフォなど)を扱ってきました。今回扱うタスク例外処理機能の観点から重要な点は、これらのAPIの多くが実行時の状況に依存しているということです。

たとえば、第3回で扱ったセマフォを用いて、以下のようにセマフォのセマフォ資源の取得を実行したとしましょう。

									tk_wai_sem( semid, 1, TMO_FEVR );
									

このとき、上記の処理をどのタスクから実行したかは極めて重要な情報です。資源がすぐに確保できない場合、上記APIを実行したタスクは資源が取得できるまで待ち状態となりますが、タスクAから実行した場合はタスクAが、タスクBから実行した場合はタスクBが待ち状態に遷移します。このように、T-KernelのAPIを実行する際の状況はAPIの動作を大きく左右する情報であり、一般にこれを「コンテキスト」と呼びます。

今回説明するタスク例外処理機能を一言で説明すると、あるタスクAから別のタスクBのコンテキストに入って操作を行うことができる、例外処理のために用意された特殊な機能です。先ほどの例で説明するならば、タスクBから tk_wai_sem() を実行し、タスクAを待ち状態に遷移させることができるような機能であるといえます。

タスク例外処理の手順

T-Kernelのタスク例外処理機能は、以下のAPIから成り立っています。

tk_def_tex タスク例外ハンドラの定義
tk_ras_tex タスク例外を発生
tk_dis_tex タスク例外を禁止
tk_ena_tex タスク例外を許可
tk_end_tex タスク例外ハンドラの終了
tk_ref_tex タスク例外の状態参照

タスク例外を発生し、対象となるタスクのコンテキストで目的のタスク例外処理を実行させるための基本的な利用手順は以下の通りとなります。

①tk_def_tex(タスク例外ハンドラの定義)により、対象タスクに対するタスク例外ハンドラを定義します。

タスク例外ハンドラは、対象となるタスクのコンテキストにおいて実行したい処理であり、その関連付けをここで行います。

②tk_ena_tex(タスク例外を許可)を用いて、そのタスクへのタスク例外の発生を許可します。

タスク例外が発生した時に、そのタスクがタスク例外を受け付けることができるように、このAPIを用いてタスク例外を許可します。

③tk_ras_tex(タスク例外を発生)を実行し、タスク例外を発生させます。

これにより、対象となるタスクのコンテクストでタスク例外ハンドラの内容が実行されます。

なお、T-Kernelのタスク例外は「タスク例外コード」という0〜31の番号による識別が可能になっており、タスク例外ハンドラはそれぞれの例外コード毎に独立に定義できます。

タスク例外処理機能を用いた例外処理

今回は、タスク例外コード0のタスク例外の利用例を示します。例外コードが0以外のタスク例外については longjmp() などを用いて復帰処理なども行うことができ、これをあわせて利用することで柔軟な例外処理が可能となりますが、複雑なので本記事では省略します。

例として、ミューテックスによる排他制御を行なっているタスクの異常を検出し、安全な状態まで戻してからタスクを終了させる処理をタスク例外で実現することとしましょう。この例では、タスク間の排他制御に用いているミューテックスのオブジェクトIDをmtxidであるものと仮定して話を進めます。

まずは、タスク例外ハンドラを定義します。タスク例外を定義するコード例は以下の通りです。

									ER ercd;
									CONST T_DTEX dtex = {
											.texatr = 0,
											.texhdr = sample_texhdr,
									};
    
									/* タスク例外ハンドラを tskid で示されるタスクに対して定義 */
									ercd = tk_def_tex(tskid, &dtex);
									if ( ercd < E_OK ) {
											goto err_ret;
									}
    
									/* タスク例外コード0のタスク例外を有効化 */
									tk_ena_tex(tskid, (1U << 0));
									

なお、上記で参照しているタスク例外ハンドラ本体の定義は以下の通りとします。

									/ *
									 * タスク例外処理
									 */
									void sample_texhdr( INT texcd )
									{
											/* ミューテックスのロックを解除するまえに、排他制御上の
												問題ない整合性のとれた状態に移行させる */
											cancel_operation();
    
											/* ミューテックスのロックを解除する */
											tk_unl_mtx(mtxid);
            
											/* タスクを終了する */
											tk_ext_tsk();
									}
									

このコードは、「あるタスクが例えばプログラム中のデッドロックを引き起こすバグにより正しく機能しなくなった状況下において、他のタスクからその状況を検出し、安全にタスクを終了させてから再起動を行い、正しい状態へと復帰させたい」というような処理を想定しています。ミューテックスのロックの解除は、取得したタスクでしか実行できませんから、タスク例外ハンドラの利用が必要となります。なお、ミューテックスのロックを解除する前に、排他制御の観点から問題ない状況への移行処理が必要となるでしょう。その内容は行いたい処理によりますが、上記のコード例ではcancel_operation() 関数でその処理を行うこととしています。

このタスク例外ハンドラを用いて、別のタスクでデッドロックを検出し、復帰処理を行うこととしましょう。コード例は以下のとおりです。

									ER ercd;
    
									/* ミューテックスのロックを試みる */
									ercd = tk_loc_mtx(mtxid, MAXIMUM_LOCK_TIME);
									if ( ercd < E_OK ) {
											if ( ercd != E_TMOUT ) goto err_ret;
            
											/* タイムアウトによるエラー(E_TMOUT) を検出した場合
												正常動作時に想定される最大ロック時間 MAXIMUM_LOCK_TIME を
												超えてしまったこととなり、デッドロックの可能性がある。
												このため、タスク例外(コード = 0)を発生させる */
											tk_ras_tex(tskid, 0);
            
											/* 復帰処理を行う */
											restart_task();
									}
									

上記の例では、ミューテックスによる最大ロック時間が MAXIMUM_LOCK_TIME であるものとし、タイムアウト時間にその値を指定しています。ここで E_TMOUT のエラーとなった場合は、正常動作時に想定される時間を超えてミューテックスのロックが解除されない状態が続いていることになるため、これはデッドロック等によりシステムが想定通り動作していない状況であると考えられます。このため、ここでは tk_ras_tex() を実行することでタスク例外を発生させ、先ほど定義したタスク例外処理を実行させます。タスク例外処理実行後の復帰処理として restart_task() 関数が実行されますが、その内容については行いたい処理に応じたものとなりますのでここでは省略します。

注意

前節の例からも分かるように、タスク例外処理機能というのはかなり特殊な機能です。プログラム中の任意の場所でタスク例外ハンドラが起動される可能性を考える必要があり、厳重な注意が必要となります。

例えば、対象タスク内で以下のような処理を行なっていたとしましょう。これは、foo() がバイナリセマフォを用いて排他制御されているという、一般的に見られるコードの形です。

									....
										/* (A) */
									tk_wai_sem(semid, 1, TMO_FEVR);
										/* (B) */
									foo();
										/* (C) */
									tk_sig_sem(semid, 1);
										/* (D) */
									....
									

ここで、タスク例外ハンドラを以下のように定義した場合どうなるでしょうか?

									void texhdr( INT texcd )
									{
											tk_wai_sem(semid, 1, TMO_FEVR); /* (E) */
											bar(); /* 終了処理 */
											tk_sig_sem(semid, 1);
            
											tk_ext_tsk();
									}
									

このタスク例外を単純に許可した場合、以下のようにタイミング上の問題が生じる可能性が出てきます。

  • ●(A),(D)のタイミングでタスク例外ハンドラが起動された場合、特に問題なく動作します。
  • ●(B),(C)のタイミングでタスク例外ハンドラが起動された場合、バイナリセマフォを同じタスクから二重に取得要求するような状況となり、永久に待ちに入ってしまいます。

このように、タスク例外は利用に注意を必要とする機能であり、どうしてもその機能が必要なときに正しい使い方で利用しなければいけません。

サブシステムとの関係

ここまでの話で分かるように、タスク例外管理機能を正しく使うのはそれほど簡単ではありませんが、サブシステムと組み合わせることにより、ミドルウェア開発の負担を大幅に減らすことが可能です。

詳細はサブシステム管理機能の範疇となりますので略しますが、T-Kernelにはサブシステム管理機能を用いて作られたソフトウェアモジュールを、正しく安全に例外処理するための仕組みが備わっています。具体的には、今回ご紹介したタスク例外に対する処理を、サブシステムごとのブレーク関数で割り込み、各ソフトウェアモジュールごとに独立して例外処理を行える仕組みがあります。

実際この仕組みを用いて例外処理を正しく行なっているミドルウェアの例としては、本連載でも簡単にご紹介したT-Kernel 2.0 Extension(T2EX)とT-Kernel Standard Extension(TKSE)があります。

興味がありましたら、T-Kernel 2.0仕様書のサブシステム管理機能におけるブレーク関数についての説明をご確認いただければと思います。