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

第07回 ディスパッチャ

お休み明けの一週間いかがでしたか?私には昨日の休日が,ことのほか嬉しく感じられました.

先週は,システム状態とコンテキストの管理について解説しました.ターゲット依存部の所もありましたが,カーネルを理解する上で大変重要な部分でした.今回も,大変重要なディスパッチャについて解説します.この部分もターゲット依存部で,さらにソースコードはアセンブラになります.まず,今週はソースコードに入る前に概要を解説し,来週から,ソースコードを見ていくことにします.
ディスパッチャの役割は実行状態のタスクを切り替える処理を行うことです.
それまで実行状態であったタスクの状態(これをコンテキストと呼ぶ)を保存し,次に実行状態にすべきタスクのコンテキストを復帰します.コンテキストとは,タスクの実行に必要なすべての情報のことを言います.具体的には,汎用レジスタ,プログラムカウンタ(以下,PCと略す) スタックポインタ(以下,SPと略す)などです.
コンテキストの保存というのは,プロセッサのレジスタの内容(今まで実行状態であったタスクのコンテキスト)をメモリへ保存し,コンテキストの復帰というのは,次実行状態になるタスクの,メモリに保存してあったコンテキストを,プロセッサのレジスタへ戻すことをいいます.
コンテキストを保存する際,すべてTCB(タスクコントロールブロックのこと.後日解説します.)に置く方法と,スタックに置く方法の両方が考えられますが,ASPは基本的に,できるだけコンテキストはスタック上に置きます.これもASPカーネルの,「RAM使用量を抑える」という基本方針からの判断です.TCBに保存する内容は,ターゲット依存部で定義でき,SHではSPとPCをTCBに保存し,残りはスタックに保存するという設計にしています.
実行状態のタスクを,タスクAからタスクBに切り替える場合の様子を【図7-1】に示します.



【図7-1 コンテキストの保存と復帰】
タスク切り替えは,何らかのサービスコールをきっかけに起こります.勝手にカーネルがタスクを切り替えることはありません.ラウンドロビンスケジューリングのように見せることもできますが,優先度ベーススケジューリングですから,優先順位を変えるサービスコール,rot_rdq()を呼び出して初めて実現します.

ディスパッチャが起動する可能性があるのは,次の3つの場合です.

1.タスクがタスク切り替えを引き起こすサービスコールを呼び出したとき
 (例)wuptsk()で,自分より優先度の高いタスクを起こした時,slp_tsk()を発行した時など
2.割込みハンドラの出口処理
 割込みハンドラ中で,タスク切り替えが必要なサービスコールを発行しても,割り込み終了時までは,保留される.【図7-2】
 その場合は,割込みハンドラの処理終了後にディスパッチャを起動する.
3.タスクの実行開始時,終了時/カーネルの動作開始時




【図7-2 遅延ディスパッチ】

ASPは,この3つのパターンによって,ディスパッチャを設計しています.
では次に,ディスパッチャの設計方針について解説します.どんな設計方針にするかは,そのOSの使用される環境や,何が求められるかによって大きく異なります.ASPカーネルでは,「タスク切り替えをできるだけ高速化する」という設計方針を選択しています.そのため最低限のレジスタだけ,保存/復帰する.という実装方法をとっています.

では,どのレジスタを保存/復帰するのが妥当でしょうか?
LinuxなどのOSでは,汎用レジスタすべてを保存/復帰しているものもありますが,資源制約,時間制約が厳しいリアルタイムカーネルではその選択はできません.できるだけ最小限のもののみを,保存/復帰する必要があります.
そこで,ディスパッチャを起動する必要があるパターンごとに,保存/復帰が必要なレジスタを整理すると以下のようになります.

保存/復帰が必要なレジスタ

タイプA:タスクがサービスコールを発行した結果,ディスパッチャが呼ばれる場合 
通常の関数呼び出しで呼び出されますので,スクラッチレジスタの保存は必要ありません.コンパイラは,スクラッチレジスタが壊れることを想定して処理しています.SH3用のGCCの場合は,R0〜R7がスクラッチレジスタとなります.

タイプB:割込みの出口処理
割込み処理は,任意のところで発生するので,全レジスタの保存が必要です.

タイプC:タスクの実行開始時,終了時/カーネルの動作開始時
レジスタの保存は必要ありません. →今回は解説対象外とします.

このように,保存/復帰が必要なレジスタのパターンは複数あります.タスク切り替えがおこる前にレジスタの保存をし,タスク切り替え後に復帰をする必要があります.つまり,ディスパッチャへの入り方,出方には複数のパターンが必要ということになります.しかし,レディキューの中で最高優先度のタスクp_schedtskを実行状態にする.という処理はすべての場合に共通です.この共通な部分を切り出して,ディスパッチャコアと呼びます.このディスパッチャコアに対して,複数の入り方と出方が存在するわけですから,ディスパッチャコア(_dispatcher)部分に対して,3つ(4つ)の入口と3つの出口を持つ構造にしています.【図7-3】

null

【図7-3 ディスパッチャコアへの入り方,出方】

ディスパッチャコアへの,入口,出口

タイプA:タスクがサービスコールを発行した結果,ディスパッチャが呼ばれる場合
_dispatchから,_dispatcherに入り,_dispatch_rに出ていく.

タイプB:割込みの出口処理
_ret_intから,_dispatcherに入り,_ret_int_rに出ていく.

タイプC:タスクの実行開始時,終了時/カーネルの動作開始時
カーネルの動作開始時 _start_dispatchから,_dispatcherに入り,_start_rに出ていく.
カーネルの終了時 _exit_and_dispatchから_dispatcherに入る.

レジスタの保存/復帰するパターンが違うので,このように3つのパターンに分けています.ですから,_dispatchからディスパッチャコアに入った場合は,dispatch_rで出ていかないと,保存されたレジスタが正しく復帰できないということになります.

ここまでの解説は,一つのタスクに注目した場合の流れになります(タイプCは除く).あるタスクだけに注目すると. _dispatchから入った場合は, _dispatc_rから出ていき,_ret_int から入った場合は,_ret_int_rから出ていく.ということになります.

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




null

.織好Aはslp_tsk()サービスコール発行により,_dispatchの実行によってタスクAのコンテキストを保存して起床待ち状態になっています.
⇒ダ菘戮旅發ぅ織好Aが起床待ち状態になったことで,タスクBが実行状態になり,wup_tsk(タスクA)を発行しました.wup_tsk()サービスコールの中で,タスク切り替えが必要な場合は,dispatch()を発行します.(第4回を参照)そこで,タスクBのコンテキストを保存します
ディスパッチャコアでタスク切り替えの処理を行い,タスクAが実行状態になります.タスクAの処理を再開するために,保存しておいたタスクAのコンテキストの復帰を行います.タスクAは,_dispatchからディスパッチャに入ったので,出ていくときは,_dispatch_rから出て,コンテキストの復帰を行います.



では,次の場合について考えてみましょう.



タスクAは先ほどの例と同じように,slp_tsk()発行によって起床待ち状態になり,タスクA より優先度の低いタスクBが実行中となっています.そこで割込みが発生し,割込みハンドラから,iwup_tsk(TASKA)によって,タスクAが起こされる場合について見ていきます.

null

.織好Aはslp_tsk()サービスコール発行により,_dispatchの実行によってタスクAのコンテキストを保存して起床待ち状態になっています.
割込み処理中に,タスク切り替えが必要なサービスコールが発行されても,ディスパッチャは起動しません(遅延ディスパッチ).ですから,割込み処理終了後,割込みハンドラの出口からディスパッチャに入ります.そして,割込み処理実行前に実行していたタスクBのコンテキストを保存します.
ディスパッチャコアでタスク切り替えの処理を行い,タスクAが実行状態になるために,保存しておいたタスクAのコンテキストの復帰を行います.タスクAは,_dispatchからディスパッチャに入ったので,出ていくときは,_dispatch_rから出ていきます.



この場合,タスクAが実行状態になるきっかけは,割込みハンドラによるサービスコール発行です.ですから,割込みハンドラ出口_ret_intからディスパッチャコアに入って処理が行われた結果,タスクAが実行状態になります.しかし,タスクAは_dispatchからディスパッチャコアに入っているので,_dispatch_rから出ていきます.(_ret_int_rではありません.)



次回は,タイプAの経路,_dispatch,_dispatcher,_dispatch_r のソースコードを解説します.