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

第08回 ディスパッチャコード解説

成人式,センター試験と続く1月は,当時の自分を思い出す機会が多くなります.懐かしい気持ちと同時に,今なら,もっと大切にできたと思う様々なことがよみがえってきます・・・

前回解説したように,ディスパッチャは,入口,ディスパッチャコア,出口の3つの処理部分にわかれます.まず,入口処理では,保存/復帰が必要なレジスタを保存します.そして,共通の処理を行う,ディスパッチャコアと呼ばれる部分があります.ここでは,実行状態のタスクのTCBを示すp_runtskにディスパッチ先のタスクのTCBを登録するという処理を行います.

復習(第3回)
p_schedtsk:レディキューの中で最高優先順位のタスクのTCBを示すポインタ(次に動くべきタスク)
p_runtsk:実行状態タスクのTCBへのポインタ

出口処理では,新しく実行状態となったタスクを実行するため,保存してあったコンテキストを復帰します.

ディスパッチャコアへの入り方,出方は,3パターンあります.これは状況によって,保存・復帰するレジスタが異なるためでした.今回は,タスクがサービスコールをよんだことによりディスパッチが必要になった場合(前回の,タイプA)に通る,入口(_dispatch),ディスパッチャコア(_dispatcher),出口(_dispatch_r)のソースコードを見ていきます.今回の解説部分は,ターゲット依存となりアセンブラで書かれています.
ちなみにSH用GCCの場合は,コンパイルするとシンボル名の前に,”_”(アンダースコア)がつきますので,アセンブラコードでは,シンボル名の前にはあらかじめ”_”をつけます.
まずそれぞれ概要を解説し,その後アセンブラソースコードを見ていきますので,必要ないところは読み飛ばしてください.

前回と同じ例で解説していきます.
優先度の高いタスクAと低いタスクBがあります.優先度の高いタスクAが実行中に,slp_tsk()を発行し,起床待ち状態になっています.次に実行状態になったタスクBが,wup_tsk(タスクA)を発行したことにより,タスク切り替えが必要になり,ディスパッチャが呼ばれる場合について見ていきます.



null

すでに,妊織好Aはコンテキストを保存して起床待ちになっていて,これから▲織好Bのコンテキストを保存して,タスクAに切り替えようという場面です.
まず,ディスパッチャコアに入る前の,入口処理部分,_dispatchについて見ていきます.
今まで実行状態だったタスクBのコンテキストを保存します.まず,保存が必要なものは,PR(プロシージャーレジスタ),汎用レジスタ,SP(スタックポインタ),PC(プログラムカウンタ)です.SHの場合は,SP,PCをTCBに保存し,残りはスタックに保存します.ソースコードの流れにそって具体的に見ていきましょう.
PRと保存が必要な汎用レジスタをスタックに積みます.保存が必要な汎用レジスタは,スクラッチレジスタ以外の,R8〜R14です(第7回参照).その後,SPをタスクBのTCBに保存します.次に,実行開始番地_dispatch_rをPCとして,タスクBのTCBに保存します.最後に,ディスパッチャコア(_dispatcher)にジャンプします.

概要を以下に示します.

STEP1:PRをスタックに保存
STEP2:保存が必要な汎用レジスタを,スタックに保存
STEP3:SPを,タスクBのTCBに保存
STEP4:実行再開番地(_dispatch_r)をPCとして,タスクBのTCBに保存
STEP5:dispatcherにジャンプ




続いて,ソースコードを読んできます.

272 _dispatch:
273 sts.l pr,@-r15 /* STEP1:PRをスタックに保存 */
274 mov.l r14,@-r15 /* STEP1:汎用レジスタR14-R8をスタックに保存 */
275 mov.l r13,@-r15
276 mov.l r12,@-r15
277 mov.l r11,@-r15
278 mov.l r10,@-r15
279 mov.l r9, @-r15
280 mov.l r8, @-r15
281 mov.l _p_runtsk_dis,r2 /* r0 <- p_runtsk */
282 mov.l @r2,r0
283 mov.l r15,@(TCB_sp,r0) /* STEP3:スタックポインタをTCBに保存 */
284 mov.l _dispatch_r_dis,r1/* STEP4:実行再開番地dispatch_rを保存 */
285 mov.l r1,@(TCB_pc,r0)
286 bra _dispatcher /* STEP5:ディスパッチャコアへジャンプ */
287 nop



ここで.前提条件の確認です.GNUコンパイラはR15をスタックポインタに割り当てます(第5回参照).ソースコード中のr15はスタックポインタとして読んでください.
関係するアセンブラについても,一緒に解説していきます.

アセンブラ(SH)の読み方
アセンブラの共通する命令フォーマットについて,下記の例で解説します.
mov.l Rm, @Rn
movが命令,その後のlは処理サイズを表します.lはロングワード(4バイト)を意味します.その次の,Rmと@Rnはオペランドです.処理は左のオペランドから,右のオペランドに向かって行われます.また,RmはレジスタRmの内容を表し,@RnはレジスタRnに格納されているアドレスが指している先の値のことです.ポインタの間接参照をイメージしてください.操作対象の指定方法について,【表7-1】にまとめます.


【表7-1 アドレッシングモードと意味】
アドレッシングモード 命令フォーマット 等価なC言語表記 主な用途
レジスタ直接 Rn Rn  
レジスタ間接 @Rn *(Rn)  
ポストインクリメント
レジスタ間接
@Rn+ *(Rn++) スタック操作
プリデクリメント
レジスタ間接
@-Rn *(--Rn) スタック操作

以降の解説では,アセンブラ命令の解説は以下のように書きます.

mov.l Rm ,@Rn  ⇒  Rm→*(Rn)


⇒の右が,アセンブラ命令の説明になります.Rmがレジスタのそのものの値を表し.(Rn)はRnが指している先の値を表します.

では,具体的にソースコードを1行ずつ見ていきます.
STEP1:PRをスタックに保存
273行目: sts.l pr,@-r15

sts.l PR,@-Rn  ⇒  PR→*(--Rn)

R15に格納されているアドレスの値を4減算してから,PRの値をR15が指しているアドレスに格納する.つまり,スタックポインタを1つ上へ進ませて,PRをスタックに積むということをしています.

STEP2:保存が必要な汎用レジスタを,スタックに保存

274行目: mov.l r14,@-r15

mov.l Rm,@-Rn ⇒  Rm→*(--Rn)

同じように,スタックポインタを1つ進ませて,R14の値をPRの上に積んでいます.
275行目:R13の値をその上に積む
  ・・・省略・・・
280行目:R8の値をその上に積む

STEP3:SPを,TCBに保存

TCBには,どこに何を格納するかはあらかじめ決まっています.格納するものは構造体のメンバとして,TCBを構成しています(詳細は後日解説します).まず,R15(スタックポインタ)を,その決められた場所に保存します.

始めに,現在実行状態であるタスクBのTCBアドレスを取得します.p_runtskは,現在実行状態であるタスクのTCBアドレスを保持していますので,p_runtskからタスクBのTCBアドレスを取得します._p_runtsk_disは,ポインタ変数p_runtskのアドレスを表します.

SHプロセッサ特有のワード/ロングワード即値の取得方法
ここで,解説します.これは,SHプロセッサ独自の制約です.SHは16ビット固定長命令を持つという特徴のため,8ビットまでの即値は命令中に直接記述することができますが,16/32ビット即値を扱うには少しテクニックが必要です. 16/32ビット即値の読み出しを行う場合には,プログラムに32ビット即値を埋め込み,PC相対でアクセスします.わかりづらいので実際の例で解説します.

438 _dispatch_r_dis:
439 .long _dispatch_r
440 _p_runtsk_dis:
441 .long _p_runtsk

アセンブラコード内に,上記のような記述をすることで,ラベル_dispatch_r_disは,シンボルdispatch_rのアドレスを表し,同じように,_p_runtask_disは,シンボルp_runtskのアドレスを表します.



281行目 mov.l _p_runtsk_dis,r2 /* r0 <- p_runtsk */
282行目 mov.l @r2,r0

mov.l Rm,Rn ⇒ Rm→Rn
mov.l @Rm,Rn ⇒ *(Rm)→Rn

_p_runtsk_disはp_runtskのアドレスを表しますので,281行目では,p_runtskのアドレスをr2に格納しています.282行目で,そのアドレスを介してp_runtaskの中身をr0に格納します.つまり,タスクBのTCBアドレスがr0に格納されます.C言語のポインタ変数の間接参照をイメージしてください.

283行目  mov.l r15,@(TCB_sp,r0)

mov.l Rm,@(disp,Rn) Rm → *(disp × 4 + Rn)




282行目で,p_runtskつまり実行状態のタスクBのTCBアドレスがR0に入っています.TCB_spというのは,TCBの中でのspの保管場所のオフセットを表します.R15はスタックポインタを意味しますので,283行目で現在のスタックポインタの値を,現在実行状態であるタスクBのTCB中で,スタックポインタを格納すべき場所に格納するという処理をしています.

STEP4:実行再開番地(_dispatch_r)を,TCBに保存

284行目 mov.l _dispatch_r_dis,r1/* STEP4:実行再開番地dispatch_rを保存 */
285行目 mov.l r1,@(TCB_pc,r0)

_dispatch_r_disは_dispatch_rのアドレスを表しています.現在実行状態であるタスクBのTCB中で,pcを格納すべき場所に_dispatch_rのアドレスを格納しています.つまり,_dispatchでコンテキストを保存したタスクは,_dispatch_rで復帰するようにしています.

STEP5:dispatcherにジャンプ
286行目 bra _dispatcher
_dispatcher(ディスパッチャコア)へジャンプします.

次にディスパッチャコア部分,_dispatcherについて見ていきます.
ここでは,次に動かすべきタスクAを実行状態にします.この時の,p_runtskと,p_schedtskは次のようになっています.



実行状態のタスクを表すp_runtskには,ディスパッチ元であるタスクBのTCBアドレスが格納されていて,次に動くべきタスクを表すp_schedtskには,ディスパッチ先であるタスクAのTCBアドレスが格納されています.
ディスパッチャコアでは,p_runtskにタスクAのTCBアドレスを格納し,次にタスクAのTCBから,SPとPCを復帰します.

STEP1: p_runtskにディスパッチ先であるタスクAのTCBアドレスを登録
(_p_schedtskがヌルでなければ)
STEP2:タスクAのTCBからスタックポインタを,R15へ復帰
STEP3:タスクAのTCBに保存してあった,実行再開番地へジャンプ




続いてソースコードを見ていきます.
348 _dispatcher:
358 mov.l _p_schedtsk_dis,r12
359 mov.l @r12,r0
360 mov.l _p_runtsk_dis,r2
361 mov.l r0,@r2 /* STEP1:schedtskをruntskに登録 */
362 cmp/eq #0,r0 /* schedtsk があるか?*/
363 bt _dispatcher_1 /* 無ければジャンプ */
364 mov.l @(TCB_sp,r0),r15 /* STEP2:TCBからタスクスタックを復帰 */
365 mov.l @(TCB_pc,r0),r1 /* STEP3:TCBから実行再開番地を復帰 */
366 jmp @r1      /* 実行再開番地へジャンプ */
367 nop
368 _dispatcher_1:
・・・・・・・・  省略



STEP1:_p_schedtskを_p_runtskへ
358 mov.l _p_schedtsk_dis,r12 /* r0 <- p_schedtsk */
359 mov.l @r12,r0 

p_schedtsk(次に実行すべきタスクのTCBアドレス)をr0へ格納します.p_schedtskには,タスクAのTCBアドレスが格納されています.p_schedtskのアドレスをいったんR12にうつして,そのアドレスを介して,_p_schedtskの中身,つまり次に実行すべきタスク(タスクA)のTCBアドレスをR0に格納しています.


360行目 mov.l _p_runtsk_dis,r2
361行目 mov.l r0,@r2 

タスクAのTCBアドレスをp_runtskに格納します.
359行目でタスクAのTCBアドレスは,R0に入っています.同じように,p_runtskのアドレスをいったんR2に格納して,R0の値をR2のアドレスが指しているところ,つまりp_runtaskへ格納します.

362行目 cmp/eq #0,r0 /* schedtsk があるか? */
363 bt _dispatcher_1 

cmp/eq #imm,R0 ⇒ R0 = imm のとき,1→T,それ以外のとき0→T
bt ⇒ Tが1のとき,分岐します

R0が0,つまりp_schedtskがヌルなら,_dispatcher_1へジャンプします.
p_schedtskがヌルのときというのは,次に実行すべきタスクがない場合です.この場合は,_dispatcher_1で,割込み待ちに入ります.この部分の解説は省略します.

STEP2:TCBからスタックポインタを,R15へ復帰
364行目 mov.l @(TCB_sp,r0),r15 /* STEP2:TCBからタスクスタックを復帰 */

R0には,359行目の処理により,次実行するタスクAのTCBアドレスが入っています.そのタスクAのTCBに保存してあったスタックポインタを,R15に復帰します.

STEP3:TCBに保存してあった,実行再開番地へジャンプ
365行目 mov.l @(TCB_pc,r0),r1 /* STEP3:TCBから実行再開番地を復帰 */
366行目 jmp @r1  

同じように,タスクAのTCBに保存してあったPCを,R1に格納します.タスクAも,_dispachから入ってレジスタを保存したので,PCには_dispatch_rが入っています.(1.ディスパッチャ入口処理(_dispatch)のSTEP4)
次に,R1が指している先,_dispatch_rにジャンプします.

ディスパッチャコアで復帰したタスクAのスタックポインタを用いて,ディスパッチャ入口でスタックに保存した値をレジスタに順番に復帰します.入口では,pr,R14,R13,R12・・・・R8という順番にスタックに積んで保存したので,復帰する場合は,R8,R9,R10・・・R14,prという順番で復帰します.

STEP1:復帰したスタックポインタを使用して,スタックポインタに積んでいた汎用レジスタを復帰
STEP2:PRを復帰
STEP3:rts命令で,PRが指しているところへジャンプ



続いてソースコードを見ていきます.

289 _dispatch_r:
290 mov.l @r15+,r8 /* STEP1:レジスタを復帰 */
291 mov.l @r15+,r9
292 mov.l @r15+,r10
293 mov.l @r15+,r11
294 mov.l @r15+,r12
295 mov.l @r15+,r13
296 mov.l @r15+,r14
297 lds.l @r15+,pr /* STEP2: PRを復帰*/
298 /*
299 * タスク例外処理ルーチンの起動
・・・・省略・・・ */
316 rts
317  nop


290行目 mov.l @r15+,r8

mov.l @Rm+,Rn ⇒ *(Rm++)→Rn

スタックポインタが指している値を,R8へ復帰して,スタックポインタを下へ進めます.
次にスタックポインタが指している値を,R9へ復帰・・・これを繰り返し,R14まで復帰します.
最後に,保存するとき一番最初に保存したPRを復帰します.


316 rts
RTS命令で,タスクAの処理を再開します.

RTS命令
サブルーチンプロシージャーから復帰します.PCをPRから復帰し,復帰したPCの示すアドレスから処理を続行します.



299行目からの,タスク例外処理ルーチンの起動の解説は省略します.