マルチプロセッサ用リアルタイムOSの解説

第17回 セマフォ資源の獲得wai_semサービスコールの解説

 

今回は,セマフォ資源の獲得サービスコールwai_semの解説をします.このAPIはASPカーネルでも提供されています.(リアルタイムOSの内部構造を見てみよう!第20回)
同期・通信オブジェクトの管理ブロックについて解説します.
ASPカーネルでは,待ち情報管理ブロックWINFO(WINFO_WOBJ)やタイムイベントブロックTMEVTB は,TCBから分離して,タスクスタック上に確保していました.これらの情報は,タスクが待ち状態でないときは必要のない情報ですので,TCBから切り離して,待ち状態となって必要な時だけタスクのスタックに領域を確保するという方法をとっていました.このようにASPカーネルではできるだけ,RAM使用量を抑えるという設計方針で設計されています.
しかし,この方法はマルチプロセッサ用RTOSには適用できません.なぜなら,タスクスタックをタスクが割り付けられているプロセッサのプライベートメモリに配置した場合,他のプロセッサからアクセスすることができません.例えば,wercd(待ち解除時のエラーコード)は,待ち解除時に解除した側のシステムコール内で格納されますので,他のプロセッサから格納する可能性もあります.その時,wercdが,他のプロセッサからアクセスできないプライベートメモリに配置されているとアクセスできません.そのため,FMPカーネルでは,待ち情報管理ブロックやタイムイベントブロックをスタック上ではなく,常にTCB内に確保しています[図1].詳細は第9回を参照してください.

セマフォのデータ構造である,セマフォ初期化ブロックとセマフォ管理ブロックの定義を以下に示します.

セマフォ初期化ブロック
[kernel/ semaphore.h]
59 typedef struct semaphore_initialization_block { 
60 ATR sematr; /* セマフォ属性 */
61 #if TTYPE_KLOCK == P_KLOCK
62 LOCK *p_obj_lock; /* オブジェクトロックへのポインタ */
63 #endif /* TTYPE_KLOCK == P_KLOCK */
64 uint_t isemcnt; /* セマフォの資源数の初期値 */
65 uint_t maxsem; /* セマフォの最大資源数 */
66 } SEMINIB;


セマフォ管理ブロック
[kernel/ semaphore.h]
75 typedef struct semaphore_control_block { 
76 QUEUE wait_queue; /* セマフォ待ちキュー */
77 const SEMINIB *p_seminib; /* 初期化ブロックへのポインタ */
78 #if TTYPE_KLOCK == F_KLOCK
79 LOCK obj_lock; /* オブジェクトロック */
80 #endif /* TTYPE_KLOCK == F_KLOCK */
81 uint_t semcnt; /* セマフォ現在カウント値 */
82 } SEMCB;


ASPカーネルとほぼ同じですが,オブジェクトロックを扱うための変数をメンバとして持っています.
プロセッサロック方式の場合は,プロセッサ単位でオブジェクトロックを管理しますので,PCBメンバとしてオブジェクトロックの実体を定義します.そこで,セマフォ初期化ブロックには,オブジェクトロックの実体であるPCB内のオブジェクトロックへのポインタを持ちます.セマフォ初期化ブロックは,コンフィギュレーション時にkernel_cfg.cにconst変数として生成されます.
下記のように,2番目のメンバにPCBのオブジェクトロックへのポインタが格納されます.
kernel_cfg.c
const SEMINIB _kernel_seminib_table[TNUM_SEMID] = {
{ (TA_TPRI), (&(_kernel_prc1_pcb.obj_lock)), (0), (1) },
{ (TA_TPRI), (&(_kernel_prc1_pcb.obj_lock)), (1), (1) },
{ (TA_TPRI), (&(_kernel_prc2_pcb.obj_lock)), (0), (1) },
{ (TA_TPRI), (&(_kernel_prc2_pcb.obj_lock)), (1), (1) }
};


細粒度ロック方式の場合は,オブジェクトごとにオブジェクトロックを管理します.そこで,セマフォ管理ブロックのメンバとして,オブジェクトロックの実体を定義します.



[図1 セマフォ待ち時のデータ構造]

[kernel/ semaphore.c]
260 ER 
261 wai_sem(ID semid)
262 {
263 SEMCB *p_semcb;
264 ER ercd;
265 PCB *my_p_pcb;
266 TCB *p_runtsk;
267
268 LOG_WAI_SEM_ENTER(semid);
269 CHECK_TSKCTX_UNL();
270 CHECK_SEMID(semid);
271 p_semcb = get_semcb(semid);
272
273 t_lock_cpu();
274 my_p_pcb = get_my_p_pcb();
275 T_CHECK_DISPATCH(my_p_pcb);
276
277 retry:
278 t_acquire_obj_lock(&GET_OBJLOCK(p_semcb));
279 if (p_semcb->semcnt >= 1) {
280 p_semcb->semcnt -= 1;
281 release_obj_lock(&GET_OBJLOCK(p_semcb));
282 ercd = E_OK;
283 }
284 else {
285    if ((my_p_pcb = t_acquire_nested_tsk_lock_self(&GET_OBJLOCK(p_semcb))) == NULL){
286 goto retry;
287 }
288 p_runtsk = my_p_pcb->p_runtsk;
289 p_runtsk->tstat = (TS_WAITING | TS_WAIT_SEM);
290 wobj_make_wait((WOBJCB *) p_semcb, p_runtsk);
291 release_nested_tsk_lock(my_p_pcb);
292 release_obj_lock(&GET_OBJLOCK(p_semcb));
293 dispatch();
294 ercd = p_runtsk->wercd;
295 }
296 t_unlock_cpu();
297
298 error_exit:
299 LOG_WAI_SEM_LEAVE(ercd);
300 return(ercd);
301 }


全体の流れを図2に示します.
(1)で説明したように,FMPカーネルのセマフォのデータ構造およびTCBは,ASPカーネルと若干異なります.データ構造が変化したことによるソースコードの違いがありますが,基本的な処理はASPカーネルと同じです. しかし,FMPカーネルでは,オブジェクトロック,タスクロックを用いて,他プロセッサとの排他制御が必要でる点が大きく異なります.



[図2 wai_sem全体の流れ]

271行目:獲得するセマフォIDから,セマフォ管理ブロックのアドレスをp_semcbに格納する.
273行目:CPUロック状態とし,割込み禁止にする.
274行目:自プロセッサのPCBのアドレスを,my_p_pcbに格納する.
278行目:オブジェクトロックを取得する.

オブジェクトロック取得マクロ GET_OBJLOCK
第14回で解説したように,ロック単位にはコンフィギュレーションパターンが3つあります.それぞれのパターンによって,オブジェクトロックの定義されている場所が異なります.そこで,オブジェクトロック取得マクロ(GET_OBJLOCK)を用意しています.

[kernel/mp.h]
#if TTYPE_KLOCK == G_KLOCK
#define GET_OBJLOCK(p_wobjcb) (giant_lock)
#else /* TTYPE_KLOCK != G_KLOCK */

#if TTYPE_KLOCK == P_KLOCK
#define GET_OBJLOCK(p_wobjcb) (*((WOBJCB*)p_wobjcb)->p_wobjinib->p_obj_lock)
#else /* TTYPE_KLOCK == F_KLOCK */
#define GET_OBJLOCK(p_wobjcb) (p_wobjcb->obj_lock)
#endif /* TTYPE_KLOCK == F_KLOCK */

#endif /* TTYPE_KLOCK != G_KLOCK */


.献礇ぅ▲鵐肇蹈奪方式の場合
#define GET_OBJLOCK(p_wobjcb) (giant_lock)
グローバル変数をオブジェクトロックとします.

▲廛蹈札奪汽蹈奪方式の場合
#define GET_OBJLOCK(p_wobjcb) (*((WOBJCB*)p_wobjcb)->p_wobjinib->p_obj_lock)
セマフォ初期化ブロックに登録した,PCBメンバをオブジェクトロックとします.

細粒度ロック方式の場合
#define GET_OBJLOCK(p_wobjcb) (p_wobjcb->obj_lock)
セマフォ管理ブロックのメンバをオブジェクトロックとします.

ロック単位のコンフィギュレーションパターンの切り替え方法
コンフィギュレーションパターンは,ターゲット依存部の target_config.h 内で,マクロ TTYPE_KLOCKに以下の3つから選択して定義します.
・ジャイアントロック方式:G_KLOCK
・プロセッサロック方式:P_KLOCK
・細粒度ロック方式:F_KLOCK

(a)セマフォ資源が獲得できた場合
279行目:セマフォの現在のカウント数が1以上の場合
280行目:カウント数を1減らす.
281行目:278行目で獲得したオブジェクトロックを解放する.

(b)セマフォ資源が獲得できなかった場合
自タスクをセマフォ獲得待ち状態とする.

285行目:t_acquire_nested_tsk_lock_sel()を発行し,2段目のロックとして,自プロセッサのタスクロックを取得する.

t_acquire_nested_tsk_lock_sel():2段目のロックとして,自プロセッサのタスクロックを取得する.
引数に1段目に取得しているセマフォのオブジェクトロック変数のアドレスを渡します.詳細は後述します.
  t_acquire_nested_tsk_lock_sel()の戻り値がNULLとなる場合
  ・ロック取得中に割込みが入り,マイグレーションした可能性がある場合
  ・該当タスクが,強制待ち状態となっている場合
戻り値がNULLの場合は,ラベルretry(277行目)から再試行します.その際には,278行目で取得したオブジェクトロックは解放されています.
また,2段目のロックの取得に成功するまで, 278行目から285行目を何度も実行する可能性があります.よって,この間は非破壊コードである必要があります.
※非破壊コードとは,変数(メモリ)の値を変更することがないコードのことをいいます.

288行目:現在実行中のタスクのTCBアドレスを,p_runtskに格納する.
289行目:現在実行中のタスクの状態を,セマフォ待ち状態とする.
290行目:同期・通信オブジェクト待ちにする共通関数wobj_make_wait()を呼び出す.
※データ構造が変化したことによるソースコードの違いがありますが,基本的な処理はASPカーネルと同じです(リアルタイムOSの内部構造を見てみよう!第19回を参照してください). 

[wobj_make_wait ()処理概要]
・make_wait()を呼び出す.
・セマフォ待ちキューに該当タスクを挿入する.
・TCBとセマフォ管理コントロールブロックを接続する.

[make_wait()の処理概要]
・make_non_runnable()を呼び出す.
・TCBのtmevtbのcallbackフィールドにNULLを指定する.
 時間待ちの際には,このフィールドに待ち解除時に呼び出す関数を指定します.NULLを指定すると永久待ちとなります.

[make_non_runnable()の処理概要]
・レディキューから該当タスクを外す.
・p_schedtskを更新する.

291行目:2段目のタスクロックを解放する.
292行目:オブジェクトロックを解放する.
293行目:実行中のタスクがセマフォ待ちとなるので,ディスパッチャを呼び出す.
294行目:293行目でディスパッチして,待ち状態になっていたタスクが,待ち解除され,実行状態になってこの行を実行する.

セマフォ待ちから実行可能状態になるのは以下の2通りの場合です.
・該当セマフォに対して,セマフォ返却サービスコールsig_sem()が発行される.
・該当タスクに対して,待ち状態の強制解除のためのサービスコールrel_wai()が発行される.

そこで,sig_sem()の中では,該当タスクのTCBの待ち解除時のエラーコード(wercd)に,E_OKを,rel_wai()の処理の中では,E_RLWAIを設定します.そうすることで,セマフォ待ち状態から解除した理由を知ることができます.

296行目:CPUロック状態を解除する.
セマフォ資源を獲得できず,自タスクをセマフォ待ち状態へ移行させる際にタスクロックを取得するために使用します(285行目).実行状態のタスクが自プロセッサのタスクロックを取得します.

<関数名>
t_acquire_nested_tsk_lock_self()
<引数>
1段目に取得しているロック変数のアドレス
<戻り値>
PCB:取得成功
NULL:
・ロック取得中に割込みが入り,マイグレーションした可能性がある場合
・該当タスクが,強制待ち状態となっている場合

・プロセッサロック方式,細粒度ロック方式
[kernel/ mp.c]
323 PCB* 
324 t_acquire_nested_tsk_lock_self(LOCK *p_objlock)
325 {
326 PCB *my_p_pcb;
327
328 my_p_pcb = get_my_p_pcb();
329 my_p_pcb->p_firstlock = p_objlock;
330 if (t_acquire_nested_lock(&(my_p_pcb->tsk_lock))) {
331 return(NULL);
332 }
333 my_p_pcb->p_firstlock = NULL;
334 if (!TSTAT_RUNNABLE(my_p_pcb->p_runtsk->tstat)){
335 x_release_lock(&(my_p_pcb->tsk_lock));
336 x_release_lock(p_objlock);
337 t_unlock_cpu();
338 t_lock_cpu();
339 return(NULL);
340 }
341 return(my_p_pcb);
342 }


全体の流れを図3に示します.



[図3 t_acquire_nested_tsk_lock_self全体の流れ]

324行目:1段目に取得しているロック変数のアドレスを受け取る.
wai_sem()から呼び出された場合は,該当セマフォのオブジェクトロックのアドレスを受け取ります.
328行目:自プロセッサのPCBアドレスを,p_my_pcbに格納する.
329行目:引数で受け取ったオブジェクトロック変数のアドレスを,取得済みロック管理変数(p_firstlock)へ格納しする.
※取得済みロック管理変数(p_firstlock)
割込み要求をチェックできないターゲットが多いため,割込みが入ったかどうかを判断するために使用する変数.詳細は,第15回を参照していください.

330,331行目:2段目のロック取得関数(t_acquire_nested_lock())の戻り値がtrueなら,NULLを返し終了.
※2段目のロック取得関数(t_acquire_nested_lock())の戻り値
true:取得中に割込みが入った場合
false:取得成功
t_acquire_nested_lock()の詳細については,第15回を参照してください.

2段目のロックの取得に成功した場合は処理を続けます.
333行目:取得済みロック管理変数(p_firstlock)をクリアする.

RUNNABLEチェック
334行目:自プロセッサの実行中のタスク(自タスク)が,実行できる状態でない場合
※この状態になるのは,割込み禁止をしてからロックを取得するまでの間に,他のプロセッサに割り付けられているタスクから,実行状態のタスクに対して,sus_tsk()が発行され,強制待ち状態となる場合が考えられます.

335,336行目:取得したタスクロック,オブジェクトロックを解放する.
337行目:CPUロック状態を解除して割込みを許可する.ここで割込みを許可することで,ディスパッチを発生させる.

◎割込みを許可する理由
sus_tsk()によって,他プロセッサのタスクを強制待ち状態とする場合は以下の処理が行われます.
(例)プロセッサ2に割り付けられているタスクから,プロセッサ1に割り付けられているタスク(TASK1_1)に対して,sus_tsk()を発行する.

・TASK1_1の状態を強制待ち状態にする
・TASK1_1をレディキューから外す
・プロセッサ1のp_schedtskを書き換える
・プロセッサ1へCPU間割込みを発生させる 

CPU間割込みが発生することによって,プロセッサ1でディスパッチャが呼ばれタスクが切り替わります[図4].



[図4 他プロセッサからsus_tsk()が発行された場合の処理]

しかし,プロセッサ1が割込み禁止中に,実行中のTASK1_1に対して,他のプロセッサに割り付けられているタスクから,sus_tsk()が発行された場合,タスクの状態は書き換えられますが,CPU間割込みが発生しません.よって,プロセッサ1では,TASK1_1のタスクの状態が書き換えられていることを知ることができず,TASK1_1が強制待ち状態のまま実行し続けます.よって,337行目でいったん割込みを許可することで,CPU間割込みを受け付け,ディスパッチを発生させます.

338行目:もう一度,割込み禁止とする.
339行目:戻り値をNULLとし終了.

341行目:RUNNABLE状態のままの場合は,自プロセッサのPCBアドレスを戻り値とし終了.

参考までに,ジャイアントロック方式の場合のソースコードを以下に掲載します.
[kernel/ mp.h]
Inline PCB* 
180 t_acquire_nested_tsk_lock_self(LOCK *p_objlock)
181 {
182 return(get_my_p_pcb());
183 }