void dispatch(void);
RTOSの中でもっとも興味を引く部分はやはりタスク切り替えです。ITRON4の場合は実は割り込みハンドラのほうが複雑な動きを要求するのですが、それでも実行コンテキストを切り替えるという魔術のような言葉の響きがタスク切り替えを際立たせています。
TOPPERS/JSPはタスク切り替えをdispatch()関数の中に閉じ込めています。このC言語関数はCPU構造に依存する関数であり、事実上すべてのターゲットでアセンブリ言語で書くことになります。dispatch()関数が呼ばれるときには必ず次の条件がすべて成立しています。
- 割り込みは禁止されている
- タスク・コンテキストである
- ディスパッチ要求が出ており、現タスクと次タスクが異なる
- C言語のランタイム環境で呼び出される
複雑な要素はすべて前もって処理されており、あとは切り替えるのみという状況で呼び出されるのです。この関数に入ると現在のタスクは必ずコンテキストが保存されます。そして、次タスクへとコンテキストが切り替わります。関数に入るときには一般関数呼び出しですが、異なるのは制御がそのまま戻ってこないことです。確かにdispatch()関数を呼び出してdispatch()関数から戻ってくるのですが、dispatch()が戻る先は別タスクです。これを実現するためにdispatch()は内部でスタックの切り替えを行います。これらはすべてCPU依存部のcpu_support.asmに実装されています。
タスク・コンテキストの保存
dispatch()に入ると直ちにコンテキストを保存します。コンテキストは次の三つです。- call preserved registers
- sp
- 実行再開番地
call preserved registersとは、VisualDSP++のC言語ランタイム環境の用語です。これらのレジスタは呼び出された関数側で責任を持って保存し、呼び出しもとに戻るときには元の値にしなければなりません。レジスタとしてはr7-r4, p5-p3, fp, retsが相当します。これらはスタックにプッシュします。
spはスタックにプッシュするわけにはいきません。このあとスタックを切り替えるのですから当たり前です。そこでspはruntsk->tskctxb.spに保存します。
最後に実行再開番地をruntsk->tskctxb.pcに保存します。ここでおもしろいのは実行再開番地が現在の実行位置とは関係ないアドレスであることです。実行再開は現在の続きではなく、退避したコンテキストを引き出すことによって実行するのがポイントです。
_dispatch: [--sp] = (r7:4, p5:3); [--sp] = rets; [--sp] = fp; p0.H = _runtsk; p0.L = _runtsk; p0 = [p0]; /* p0 を runtsk に */ [p0+TCB_sp] = sp; /* タスクスタックを保存 */ p1.H = dispatch_r; /* 実行再開番地を保存 */ p1.L = dispatch_r; [p0+TCB_pc] = p1; jump dispatcher;
タスク・コンテキストの切り替え
次に変数runtskにschedtskの値をコピーします。schedtskは次に実行すべきタスクの制御ブロックへのポインタです。切り替え先タスクを設定したら、runtsk->tskctxb.spの値をスタック・ポインタとして設定します。そしてruntsk->tskctxb.pcから実行再開番地を取り出してジャンプします。
dispatcher: /* * ここでは割込み禁止状態でなければならない. */ p0.H = _schedtsk; p0.L = _schedtsk; p1.H = _runtsk; p1.L = _runtsk; r0 = [p0]; [p1] = r0; // schedtsk を runtskに cc = r0; if !cc jump dispatcher_1; // runtskが無ければ割り込み待ちに。 p0 = r0; // p0はruntsk sp = [p0+TCB_sp]; // タスクスタック復帰 p1 = [p0+TCB_pc]; // 実行再開番地復帰 jump (p1); // 実行再開番地へ飛ぶ
タスク・コンテキストの復帰
ここまででタスク・コンテキストのうちspとpcは復帰しています。そこでfpを復帰してC言語の呼び出し環境を整えた上で、必要に応じてタスク例外のフックを呼び出します。最後に保存していた戻り番地とレジスタを復帰してdispatch()から戻ります。
dispatch_r: fp = [sp++]; p1.H = _runtsk; p1.L = _runtsk; p0 = [p1]; r0 = [p0+TCB_texptn]; // runtsk->texptn cc = r0; cc = !cc; // texptnが0なら1 r0 = [p0+TCB_enatex]; r1.L = TCB_enatex_mask&0xffff; r1.H = TCB_enatex_mask>>16; r0 = r0 & r1; cc |= az; // texptrnが0か、enatexが0なら if cc jump dispatch_r_1; /* そのときは即リターン */ call _call_texrtn; dispatch_r_1: rets = [sp++]; // dispatch()から戻る (r7:4, p5:3) = [sp++]; rts; _dispatch.end:
こうしてみるとタスクの切り替え最中はスタックが「ない」状態で走っています。ギヤをニュートラルに入れたまま惰性で走行しているようなもので、これがdispatch()に入る時点で割り込みが禁止される理由です。
また、上のコードを見てみると次のような点に気づきます。
- runtskのアドレスが繰り返しロードされているが、共用できないか
- タスクの実行再開番地はdispatch_rの決め打ちでいいのではないか
dispatch()の実装はm68k実装のロジックをそのまま持ってきただけですので、これらはこれから検討すべき点です。しかし今の段階では最適化よりもまず動かすことが優先事項です。