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

第14回 サービスコールのコード解説(act_tsk ,iact_tsk)

マイケル・ジャクソンの,スリラー25周年記念アルバムを買いました.高校2年生の創作ダンスの授業で,私たちのクラスがテーマ曲として選んだのがスリラーでした.20人くらいの女子が集まって,放課後毎日ダンスの練習をしたのを覚えています.当時の私にとって,マイケルは衝撃的でした.でも,やっぱり今聴いてもかっこいい!


今回と次回は代表的なサービスコールの解説をしていきます.今回は,act_tsk(),iact_tsk()について解説します.まず,このサービスコールの仕様について解説します.
まず,act_tsk()とiact_tsk()の仕様を,μITRON4.0の仕様書で確認します.

ER ercd = act_tsk( ID tskid );
ER ercd = iact_tsk( ID tskid );

【パラメータ】 
ID tskid 起動対象のタスクID番号

【リターンパラメータ】
ER ercd 正常終了(E_OK)またはエラーコード

【エラーコード】
E_ID 不正ID番号(tskidが不正,または使用できない)
E_NOEXS オブジェクト未生成
E_QOVR キューイングオーバーフロー

【機能】
tskidで指定されるタスクを起動します.具体的には,対象タスクを休止状態から実行可能状態に移行させ,タスクの起動時に行うべき処理を行います.タスクを起動する際のパラメータとして,タスクの拡張情報を渡します.
対象タスクが休止状態でない場合には,タスクに対する起動要求をキューイングします.具体的には,タスクの起動要求キューイング数に1を加えます.タスクの起動要求キューイング数に1を加えると起動要求キューイング数の最大値を超える場合には,E_QOVRエラーを返します.(ASPカーネルでは,起動要求キューイング数は1を採用しています.)
tskidにTSK_SELF(=0)が指定されると,自タスクを対象タスクとします.しかし非タスクコンテキストからの呼び出しでこの指定が行われた場合には,E_IDエラーを返します.(以上,μITRON4.仕様書より抜粋)


以下に,タスクA(優先度高),タスクB(優先度中),タスクC(優先度低)を用いてact_tsk()サービスコール発行によるタスクの状態の変化を説明します.
まず初期状態を,タスクAは休止状態,タスクBは実行状態,タスクCは実行可能状態とします.
タスクBがact_task(タスクA)を発行したことによる状態の変化を【図14-1】に示します.



【図14-1 3タスクのact_tsk()発行による状態の変化】

まず,初期状態を【図14-2】で表します.



【図14-2 初期状態】

実行可能状態と,実行状態のタスクは,レディキューにつながっています.初期状態のレディキューの様子を【図14-3】に示します.タスクAは休止状態なのでレディキューにはつながっていません.



【図14-3 レディキューの初期状態】

act_tsk()

act_tsk()のソースコードを見ていきます.

< kernel/task_manage.c>
57 ER
58 act_tsk(ID tskid)
59 {
60 TCB *p_tcb;
61 ER ercd;
62
63 LOG_ACT_TSK_ENTER(tskid); 
64 CHECK_TSKCTX_UNL();
65 CHECK_TSKID_SELF(tskid); 
66 p_tcb = get_tcb_self(tskid); 
67
68 t_lock_cpu(); 
69 if (TSTAT_DORMANT(p_tcb->tstat)) {
70 if (make_active(p_tcb)) {
71 dispatch();
72 }
73 ercd = E_OK;
74 }
75 else if (!(p_tcb->actque)) {
76 p_tcb->actque = TRUE;
77 ercd = E_OK;
78 }
79 else {  
80 ercd = E_QOVR;
81 }
82 t_unlock_cpu();
83
84 error_exit:
85 LOG_ACT_TSK_LEAVE(ercd);
86 return(ercd);
87 }


act_tsk()の概要を,【図14-4】に示します.



【図14-4 act_tsk()概要】

66行目:起動させるタスクのTCBへのポインタを取得します.今回はタスクAのTCBアドレスです.act_tsk()サービスコールは,引数(ID tskid)に起動させるタスクのIDを受け取ります.この行では,p_tcbにこれから起動するタスクのTCBアドレスを格納します.
get_tcb_self()は,そのIDが0(自分自身)の場合はp_runtskを,そうでない場合は,tskidをindexにして,tcb_table[]からTCBアドレスを取得し返すマクロです.

※参考
#define get_tcb_self(tskid) ((tskid) == TSK_SELF ? p_runtsk : get_tcb(tskid))
今回は,タスクBがact_tsk(タスクA)を発行したので,p_tcbにはタスクAのTCBアドレスが格納されます.

68行目:CPUロック状態にします.
69行目:TSTAT_DORMANTは,タスクの状態を見て,休止状態かどうかを確認するマクロです.タスクの状態は,TCBのtstatに格納されていました.休止状態は,TS_DORMANTと定義されています(第13回参照).タスクAは休止状態なのでTRUEです.
70行目:make_active()では,レディキューにタスクAのTCBを挿入し,その結果ディスパッチが必要になったかどうかを戻り値とします.詳細は後で解説します.

75行目:休止状態でなく,起動要求されていなければ
TCBの起動要求キューイング(actque)メンバは,起動要求されているかどうか返すBOOL型変数です.ITRON仕様のスタンダードプロファイルでは、キューイング回数は最低1回としています。ASPカーネルはキューイング回数を1回としていますので,キューイングされているかどうかを,BOOL型変数で表現します.
76行目:起動要求キューイングをTRUEにします.
79行目:すでに,起動要求キューイングされている場合は,エラーコードにE_QOVERを設定します.

この関数はact_tsk()からは,対象タスクが休止状態のときに呼ばれます.activate_contextを呼び出し,タスクの起動準備を行い,make_runnable()を呼び出すこととで,タスクAを休止状態から,実行できる状態にし,タスクAのTCBをレディキューに挿入します.その結果,ディスパッチが必要になったかどうかを戻り値とします.


309 BOOL
310 make_active(TCB *p_tcb)  
311 {
312 activate_context(p_tcb);   
313 return(make_runnable(p_tcb)); 
314 }
ここでは,タスクの起動準備を行います.TCBの情報のうち,ターゲット依存のtskctxb(タスクコンテキストブロック)を初期化します.タスクコンテキストブロックは構造体で,そのメンバはスタックポインタ,プログラムカウンタでした.(第12回参照)

73 typedef struct task_context_block {
74 void *sp; /* スタックポインタ */
75 FP pc; /* プログラムカウンタ */
76 } CTXB;

このスタックポインタやプログラムカウンタの大きさなどもターゲット依存ですので,これらを初期化するactivate_context()は,ターゲット依存部で定義されています.active_context()では,このスタックポインタとプログラムカウンタを初期化します.


373 #define activate_context(p_tcb)   
374 {
375 (p_tcb)->tskctxb.sp = (void *)((char *)((p_tcb)->p_tinib->stk)
376 + (p_tcb)->p_tinib->stksz); \
377 (p_tcb)->tskctxb.pc = (void *) start_r;
378 }


タスクの初期化情報として,TINIBにスタック領域のサイズと,スタック領域の先頭番地が定義されています.(第12回参照)
stksz スタック領域のサイズ
*stk スタック領域の先頭番地

activate_context()の概要を【図14-5】に示します.



【図14-5 タスクの起動準備】

375行目:STEP1
該当タスク用のスタックポインタを,スタック領域の先頭番地にスタック領域を足したものに設定します.つまり,タスクが使用できるスタックの一番下位のアドレスを指すようにします.

377行目:STEP2
プログラムカウンタに,start_rを設定します.このstart_rは,タスク起動時の処理が書かれています. dispatch()が呼ばれてこのタスクが起動するときに,start_rの処理が行われます.

make_runnable()はmake_active()から呼ばれました.詳細は,第13回で解説しました.

簡単にまとめると,

該当タスクの状態を,RUNNABLEとする

該当タスクのTCBをレディキューに挿入する

p_schedtskの更新が必要なら,更新
※p_schedtskの更新が必要な場合
今まで実行できるタスクがなかったとき,もしくは,p_schedtskの優先度より該当タスクの優先度の方が高い(小さい)場合です.

p_schedtsを更新した場合は,dspflgを戻り値とする.更新しなかった場合は,FALSEを戻り値とする.

つまり,この関数の戻り値は,ディスパッチできる状態かつ,p_schedtskが更新された場合は,TRUE,それ以外は,FALSEとなります.

タスクAをレディキューに挿入した状態を【図14-6】に示します.



【図14-6 タスクAが挿入されたレディキュー】

今回の例では,新たにレディキューに挿入されたタスクAは,今まで実行状態であったタスクBより優先度が高いため,p_schedtskが更新されます.よって,ディスパッチが必要となったので,make_runnable()の戻り値がTRUEとなり,引き続きmake_active()の戻り値もTRUEとなります.

ここまでの処理を行った後に,ディスパッチャが呼ばれます.(act_tsk 71行目 ) 
ディスパッチャの処理を思い出してください.(第7回参照)

act_tskの処理の中で,dispatch()を呼び出すまでは,タスクBのコンテキストで動作しています.dispatch()が呼ばれることで,【図14-7】の_dispatchに処理がうつります.

_dispatch処理概要
タスクBのコンテキストを保存します.
_dispatcherの処理概要
・タスクAのTCBに設定してあるspの値を,実行環境のspに格納し有効にします.タスクAのspは,activate_context()でタスクAのスタックの初期値に設定してあります. 
・タスクAのTCBに設定してあるpcにジャンプします.activate_context()で,start_rに設定しているため,start_rに分岐します.
※ここでの処理により,タスクAのTCBアドレスがR0に格納されています.

null

【図14-7 ディスパッチャ出入り口処理概要】

ディスパッチの結果,3つのタスクの状態を,【図14-8】に示します.



【図14-8 ディスパッチ後の状態】
この処理は,最初にタスクが起動し,スケジューリングされたときに行われます.つまり,act_tsk()発行時にすぐ動くわけではなく,該当タスクが実行状態になってはじめて動く部分です.【図14-7】における,△離織ぅ潺鵐阿任后

STEP1:戻り番地をext_tskに設定
STEP2:TINIBに設定されているタスクの起動番地にジャンプ
STEP3:TINIBに設定されている拡張情報をR4に設定


497 _start_r:
498 mov.l _lock_flag_start, r1 /* CPUロック解除状態に */
499 xor r2,r2
500 mov.l r2,@r1
501 mov.l _mask_md_start,r4 /* 割り込み許可 */
502 ldc r4,sr
503 mov.l _ext_tsk_start,r2 /* 戻り番地を設定 */
504 lds r2,pr
505 mov.l @(TCB_p_tinib,r0),r3 /* p_runtsk->p_tinibをr3に */
506 mov.l @(TINIB_task,r3),r1 /* タスク起動番地をr1に */
507 jmp @r1
508 mov.l @(TINIB_exinf,r3),r4 /* exinfを引数レジスタr4に */
509
510 .align 4
511 _lock_flag_start:
512 .long _lock_flag
513 _mask_md_start:
514 .long 0x40000000
515 _ext_tsk_start:
516 .long _ext_tsk



503,504行目:STEP1
PRに,タスクの終了処理サービスコール(ext_tsk)のアドレスを格納しています.明示的にext_tsk()を呼び出さなくても,この処理によってタスクが終了すると,ext_tsk()の処理が行われます.

505行目:_dispatcherで,タスクAのTCBアドレスがR0に格納されていますので,TCBからTINIBのアドレスを取得し,R3に格納します.TCB_p_tinibは,TCBにおけるTINIBのアドレスを指すメンバのオフセットです.

506,507行目:STEP2
TINIBに格納されている,タスクの起動番地にジャンプします.TINIB_taskは,TINIBにおけるタスク起動番地を表すメンバのオフセットです.

508行目:STEP3
TINIBに格納されている拡張情報を,R4に格納します.TINIB_exinfは,TINIBにおけるタスクの拡張情報を表すメンバのオフセットです.
iact_tsk()もタスクの起動のためのサービスコールです.先頭にiがつくサービスコールは,非タスクコンテキストから呼び出します.機能はact_tsk()とほとんど同じですが,呼び出し元のコンテキストの違いによる処理の違いがあります.

iact_tsk()のソースコードを見てみましょう.

< kernel/task_manage.c>
  96 ER
97 iact_tsk(ID tskid)
98 {
99 TCB *p_tcb;
100 ER ercd;
101
102 LOG_IACT_TSK_ENTER(tskid);
103 CHECK_INTCTX_UNL();
104 CHECK_TSKID(tskid);
105 p_tcb = get_tcb(tskid);
106
107 i_lock_cpu();
108 if (TSTAT_DORMANT(p_tcb->tstat)) {
109 if (make_active(p_tcb)) {
110 reqflg = TRUE;
111 }
112 ercd = E_OK;
113 }
114 else if (!(p_tcb->actque)) {
115 p_tcb->actque = TRUE;
116 ercd = E_OK;
117 }
118 else {
119 ercd = E_QOVR;
120 }
121 i_unlock_cpu();
122
123 error_exit:
124 LOG_IACT_TSK_LEAVE(ercd);
125 return(ercd);
126 }


act_tsk()とほとんど同じであることがわかると思います.それでは,異なる点を解説します.

<act_tsk()との違い>
呼び出し元のコンテキストの違いによる処理の違い2点を解説します.

ヽ催タスクのTCBアドレス取得方法
105行目:p_tcb = get_tcb(tskid);
※act_tsk()では
66行目:p_tcb = get_tcb_self(tskid); 


act_tsk()の場合は,自分自身から呼ばれた場合について考慮していましたが,iact_tsk()では対象タスクが自分自身ということはありえませんので,単純にtskidをindexにして,tcb_table[]からTCBアドレスを取得します.


▲妊スパッチが必要な場合の処理
110行目:reqflg = TRUE;
※act_tsk()では,
71行目: dispatch();

iact_tsk()の場合は,ディスパッチが必要な場合,ここでディスパッチせずreqflgをTRUEにするだけです.割込みハンドラ(非タスクコンテキスト)はディスパッチャより優先度が高いので,割込みハンドラ処理中にディスパッチが必要になっても,割込み終了まで保留されます.(遅延ディスパッチ)
その後,割込み出口の処理で,reqflgがTRUEなら,ディスパッチャを呼び出します.(第10回参照)