リアルタイムOSの内部構造を見てみよう!

第18回 サービスコールのコード解説(tslp_tsk)

最近ハーブに,はまっています.ラベンダーなどの苗をいくつか,ルッコラなど食用ハーブもいく種類か種をまきました.また,美容のためにハーブティーを毎日飲むようになりました.
植物を植えると,とたんに朝起きるのが楽しみになります.



前回は,自分を起床待ちにするサービスコール(slp_tsk)の解説をしました.今回は,自分を起床待ちにするサービスコールのタイムアウト機能付き(tslp_tsk)の解説を行います.
tslp_tskサービスコールは,slp_tskと違って引数を持ちます.

ER ercd = tslp_tsk(TMO tmout);

tslp_tskのパラメータtmoutについて解説します.tmoutには,正の値,TMO_POL(=0),TMO_FEVR(=-1)を指定することができます.

正の値:タイムアウト値を表します.指定したミリ秒後に起床します.
0(TMO_POL):ポーリングを表します.待とうとしてエラーならすぐリターンします.
-1(TMO_FEVR):永遠に待つといういう意味になります.

実は,tslp_tsk(TMO_FEVR)とすると,slp_tskと同じ機能になります.ですから,tslp_tskがあればslp_tskは必要ないという設計もあると思います.しかし,tslp_tskしか用意されていないと,slp_tskの機能しか使わない場合は,余分なもの(タイムイベント管理をする機能や,タイムイベントブロックに要する領域)が必要ないのにくっついてきてしまいます.そこでASPカーネルでは,tslp_tskとslp_tskを分けています.

では,tslp_tskのソースコードを見ていきます.

/kernel/task_sync.c
92 ER
93 tslp_tsk(TMO tmout)
94 {
95 WINFO winfo;
96 TMEVTB tmevtb; 
97 ER ercd;
98
99 LOG_TSLP_TSK_ENTER(tmout);
100 CHECK_DISPATCH();
101 CHECK_TMOUT(tmout); 
102
103 t_lock_cpu();
104 if (p_runtsk->wupque) {
105 p_runtsk->wupque = FALSE;
106 ercd = E_OK;
107 }
108 else if (tmout == TMO_POL) { 
109 ercd = E_TMOUT; 
110 }
111 else {
112 p_runtsk->tstat = (TS_WAITING | TS_WAIT_SLP);
113 make_wait_tmout(&winfo, &tmevtb, tmout);
114 LOG_TSKSTAT(p_runtsk);
115 dispatch();
116 ercd = winfo.wercd;
117 }
118 t_unlock_cpu();
119
120 error_exit:
121 LOG_TSLP_TSK_LEAVE(ercd);
122 return(ercd);
123 }



95行目:winfo(待ち情報管理ブロック)をローカル変数上に確保します.slp_tskと同じです.
96行目:tmevtb(タイムイベントブロック)をローカル変数上に確保します.このコードはslp_tskにはありませんでした.slp_tskはタイムアウトしないのでタイムイベントブロックを必要としないので,確保しませんでしたが,tslp_tskでは必要となります.

101行目:指定された引数が不正の場合は,WINFOのエラーコードにE_PARを格納します.

104行目〜107行目:該当タスクの起床待ちキューイングがTRUEの場合の処理です.slp_tskと同じです.キューイング数をFALSEにして,起床待ち状態に入らずに実行状態のまま処理を続けます.

108行目:引数tmoutに,TMO_POLが指定された場合
109行目:起床待ちに入らず,エラーコードにE_TMOUTを設定します.

111行目〜117行目:引数が正の値とTMO_FEVRの場合
ここの処理もほとんどslp_tskと同じです.slp_tskでは,make_wait(&winfo)を読んでいたのに対して,ここでは,113行目でmake_wait_tmout(&winfo, &tmevtb, tmout);を呼び出しています.make_wait_tmout()はmake_wait()同じように,待ち状態へ移行する関数ですが,make_wait()の引数がwinfoだけだったのに対して,make_wait_tmout()の引数は,加えてタイムイベントブロックのアドレスとタイムアウト値が必要となります.
115行目:今まで実行状態だったタスクが,待ち状態となるので,必ずディスパッチが必要となります.ここで,ディスパッチャを呼び出します.

116行目:この処理が行われるのは,起床待ち状態の該当タスクに対して,タイムアウトした場合,または,wup_tskが発行された場合,rel_wai(強制待ち解除)が発行された場合のいずれかです.その結果ディスパッチが実行され,該当タスクが実行状態になった場合,すなわちdispatch()からリターンした後に実行されます.



次に,make_wait_tmout()の解説をします.
この関数では,TCBと待ち情報管理ブロック(WINFO),待ち情報管理ブロックとタイムイベントブロック(TMEVTB)を接続します.そして,TMEVTBに必要な情報を登録する関数を呼び出します.

55 void
56 make_wait_tmout(WINFO *p_winfo, TMEVTB *p_tmevtb, TMO tmout)
57 {
58 (void) make_non_runnable(p_runtsk);
59 p_runtsk->p_winfo = p_winfo; 
60 if (tmout > 0) { 正
61 p_winfo->p_tmevtb = p_tmevtb; 
62 tmevtb_enqueue(p_tmevtb, (RELTIM) tmout,
63 (CBACK) wait_tmout, (void *) p_runtsk);
64 }
65 else {forever
66 assert(tmout == TMO_FEVR);
67 p_winfo->p_tmevtb = NULL;
68 }
69 }




58,59行目はslp_tskから呼ばれるmake_wait()での処理と同じです.
58行目:make_non_runnable()は,レディキューから該当タスクのTCBを外します.そして,p_schedtskを更新します.
59行目:tslp_tskの中でスタック上に確保した,WINFOのための領域p_winfoをTCBに接続します.

60行目:tmoutが正,つまり,タイムアウトする場合
61行目:tslp_tskで確保した,タイムイベントブロックのアドレスを,該当タスクのWINFOに格納することで接続します.(図1)

null

【図1 tslp_tskで使用するデータ構造】

62行目:tmevtb_enqueue関数にて,タイムイベントブロックに,必要な情報を登録します.さらに,tmevtb_enqueue中で呼ばれる関数で,タイムイベントブロックをタイムイベントヒープに挿入します.

65行目:tslp_tskの引数がTMO_FEVRの場合は機能はslp_tskと同じです.
67行目:タイムイベントブロックは使用しないので,winfo->p_tmevtbにNULLを設定してタイムイベントブロックと接続しません.


次に,tmevtb_enqueue()の解説をします.
タイムイベントブロックには,タイムヒープ中での位置,タイムアウトしたときに呼び出すコールバック関数,そのコールバック関数に渡す引数を登録します.(図2)



【図2 タイムイベントブロックの構造】

この関数では,まず,コールバック関数,コールバック関数に渡す引数を登録します.

kernel/time_event.h
166 Inline void
167 tmevtb_enqueue(TMEVTB *p_tmevtb, RELTIM time, CBACK callback, void *arg)
168 {
169 assert(time <= TMAX_RELTIM);
170
171 p_tmevtb->callback = callback;
172 p_tmevtb->arg = arg;
173 tmevtb_insert(p_tmevtb, base_time + time);
174 }




171,172行目:ここではまず,コールバック関数に,wait_tmout関数を,引数に_p_runtskを登録します.
173行目:このタイムイベントブロックをタイムイベントヒープに挿入する関数,tmevtb_insert()を呼び出します.タイムイベントブロックと,タイムアウト時間を引数にして,tmevtb_insert関数を呼び出します.tslp_tskの引数には,今から○○ミリ秒後という相対時間を指定しますが,ここでは絶対時間(base_timeをたしたもの)を引数に指定します.ここで,コールバック関数に,wait_tmout関数を登録することで,タイムアウト時刻になると,wait_tmout関数が発行されるという仕組みです.


次に,tmevtb_insert()の解説をします.
タイムイベントノードは,イベント発生時刻と対応するタイムイベントブロックをメンバに持つ構造体で定義されています.このタイムイベントノードが,発生時刻順に並んだ配列が,タイムイベントヒープです.(図3)
tmevtb_insert()では,tmevtb_enqueue()で必要情報を登録したタイムイベントブロックと,イベント発生時刻を対にしたタイムイベントノードを生成し,そのタイムイベントノードをタイムイベントヒープの該当場所に挿入します.



【図3 タイムイベントヒープの構造】

ここでは,イベント発生時刻順にならんだヒープの中で,該当タイムイベントノードを挿入する位置を検索し,挿入します.

kernel/time_event.c
214 void
215 tmevtb_insert(TMEVTB *p_tmevtb, EVTTIM time)
216 {
217 uint_t index;
218
219 /*
220 * last_index をインクリメントし,そこから上に挿入位置を探す.
221 */
222 index = tmevt_up(++last_index, time);
223
224 /*
225 * タイムイベントを index の位置に挿入する.
226 */
227 TMEVT_NODE(index).time = time;
228 TMEVT_NODE(index).p_tmevtb = p_tmevtb;
229 p_tmevtb->index = index;
230 }



222行目:該当タイムイベントノードを挿入する位置をindexに格納します.これが,タイムイベントヒープ配列の添え字となります.
227行目:引数で受け取った,イベント発生時刻をタイムイベントノードに格納します.
228行目:タイムイベントノードに,タイムイベントブロックのアドレスを格納します.
229行目:この関数で決定されたタイムイベントヒープ中の位置を,タイムイベントブロックに登録します.

次に,タイムアウト時間になったら呼ばれるように,タイムイベントブロックのコールバック関数に登録した,wait_tmout関数の解説をします.
この関数では,WINFOのエラーコードに,E_TMOUTを格納します.次に,二重待ちかどうか判断 二重待ちなら強制待ちへ,そうでなければ実行できる状態にします.



kernel/wait.c
121 void
122 wait_tmout(TCB *p_tcb)
123 {
124 wait_dequeue_wobj(p_tcb);
125 p_tcb->p_winfo->wercd = E_TMOUT;
126 if (make_non_wait(p_tcb)) { 
127 reqflg = TRUE;
128 }
129
133 i_unlock_cpu();
134 i_lock_cpu();
135 }



124行目:セマフォ待ちのような場合には,セマフォ待ちキューから該当タスクのTCBを外す必要があります.このように,該当タスクがオブジェクト待ちキューにつながっている場合は,そのキューから外す関数(wait_dequeue_wobj)を呼び出します.今回のtslp_tskの場合は,オブジェクト待ちでないので,この関数を呼び出す必要はありません.しかし,この関数はオブジェクト待ちからタイムアウトする場合にも呼び出されますのでここに必要です.

125行目:WINFOのエラーコード(wercd)に,E_TMOUTを格納します.

126行目:make_non_waitは待ち状態から実行できる状態にして,ディスパッチが必要であれば,TRUEを戻り値とします.(詳細は,第4回を参照)
127行目:この関数は割込みから呼ばれるので,ここではディスパッチせず,reqflgをTRUEにします.割込みハンドラ(非タスクコンテキスト)は,ディスパッチャより優先度が高いので,割込みハンドラ処理中にディスパッチが必要になっても,割込み終了まで保留されます(遅延ディスパッチ).割込み出口処理で,reqflgがTRUEなら,ディスパッチャを呼び出します.(第10回参照)


次に,今回は呼び出す必要性はないですが,オブジェクト待ちの場合に必要なので存在しているwait_dequeue_wobj関数の解説をします.

105 Inline void
106 wait_dequeue_wobj(TCB *p_tcb)
107 {
108 if (TSTAT_WAIT_WOBJ(p_tcb->tstat)) {
109 queue_delete(&(p_tcb->task_queue));
110 }
111 }



108行目:該当タスクがなんらかのオブジェクト待ちキューにつながっているのであれば
109行目:該当タスクのTCBをオブジェクト待ちキューから外す関数(queue_delete())を呼び出します.今回のtslp_tskの場合は,オブジェクト待ちではないので,queue_delete()は呼び出しません.

今回はtslp_tsk()の発行により, wait_dequeue_wobj関数が呼ばれましたが,この場合は,オブジェクト待ちキューにはつながっていないので,108行目のif文の条件に当てはまらず,何も処理をせずにこの関数から,returnします.

では,108行目のオブジェクト待ちキューにつながっているかどうかは,どうやって判断するのでしょうか?

108行目で実行している,TSTAT_WAIT_WOBJ(p_tcb->tstat) マクロは次のように定義されています.

117 #define TSTAT_WAIT_WOBJ(tstat) (((tstat) & TS_WAIT_MASK) >= TS_WAIT_RDTQ)

第16回で解説しましたが,タスクの状態は以下のようにTCB中のtstatで管理しています.





tstatのうち,4ビットを使って待ち状態を区別しています.
そして,以下のようにそれぞれの待ち要因を定義しています.

/kernel/task.h
72 #define TS_WAIT_DLY     (0x00U << 3)    /* 時間経過待ち */
73 #define TS_WAIT_SLP (0x01U << 3) /* 起床待ち */
74 #define TS_WAIT_RDTQ (0x02U << 3) /* データキューからの受信待ち */
75 #define TS_WAIT_RPDQ (0x03U << 3) /* 優先度データキューからの受信待ち */
76 #define TS_WAIT_SEM (0x04U << 3) /* セマフォ資源の獲得待ち */
77 #define TS_WAIT_FLG (0x05U << 3) /* イベントフラグ待ち */
78 #define TS_WAIT_SDTQ (0x06U << 3) /* データキューへの送信待ち */
79 #define TS_WAIT_SPDQ (0x07U << 3) /* 優先度データキューへの送信待ち */
80 #define TS_WAIT_MBX (0x08U << 3) /* メールボックスからの受信待ち */
81 #define TS_WAIT_MPF (0x09U << 3) /* 固定長メモリブロックの獲得待ち*/


この,待ち要因の値の並べ方がここに関係してきます.
TS_WAIT_MASKは,次のように定義されています.

#define TS_WAIT_MASK ( 0x0fU << 3 )

つまり,(tstat) & TS_WAIT_MASKとすることで,tstat中の待ち要因を取り出しています.その待ち要因が,TS_WAIT_RDTQ以上ということは,上記定義の並べ方から,時間経過待ち,起床待ちではないことを意味します.つまり,待ち要因がTS_WAIT_RDTQ以上であるということは,なんらかのオブジェクト待ちであることを表しています.このようにに使うために,待ち要因の並べ方には工夫がされています.


tslp_tskサービスコールからの戻り方と,その際のエラーコードを以下に示します.

・引数のタイムアウト時間が不正の場合 E_PAR
・引数にTMO_POLが指定された場合 E_TMOUT
・タイムアウトした場合 E_TMOUT
・wup_tskサービスコールで起床待ち解除となった場合 E_OK
・rel_waiサービスコールで強制的に待ち解除となった場合 E_RLWAI
最後に,tslp_tsk(TMO tmout)発行により.タイムアウトつき起床待ち状態にある場合の,データ構造に注目して流れをまとめます.

STEP1:tslp_tskを発行したタスクのスタック領域に,WINFOとTMEVTB領域を確保し,接続します(図4).

STEP2:TMEVTBに,コールバック関数とその引数を登録します.具体的にはコールバック関数には,wait_tmout()を,引数にはp_runtskを登録します(図4).

null

【図4 STEP1,STEP2】

STEP3:タイムイベントヒープ中の,イベント発生時刻に対応した位置に,TMEVTBを挿入します(図5).

STEP4:イベント発生時刻になると,タイムイベントハンドラから,TMEVTBに登録したwait_tmout(p_runtsk)が呼び出します(図5).



【図5 STEP3,STEP4】

STEP1〜STEP4を時間の流れと,タスクと割込みハンドラの関係を,図6に示します.tmoutミリ秒後に発生するタイムイベントハンドラから呼び出される関数の中では,reqflgをTRUEとし,割込み処理終了後,ディスパッチが発生し,タスクが起床します.



【図6 tslp_tsk発行からタスク起床までの流れ】