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

第10回 割込みハンドラの出口処理の役割(前半)

今年の節分は休日だったので,恵方寿司を娘と巻きました.無病息災を願って恵方寿司を食べ,楽しく豆まきをしました.この恵方寿司,私はコンビニで売り出されているのを見て,初めてその風習を知りました.すたれていく風習が多い中で,こんな楽しい習わしを全国的に有名にした,コンビニの功績は大きいと思います.

前回は割込み処理入口の解説をしました.今回は割込み出口の解説を行います.まず,入口処理から,出口処理までの一連の流れの,概要をつかんでいただきたいと思います.
割込みが発生すると,SHプロセッサが決められた処理を行って,VBR+0x600番地にジャンプします.(【図10-1】の 



【図10-1 割込み処理の流れ】

次に,VBR+0x600番地に置かれた例外処理ルーチンを,ASPカーネルが実行します.(【図10-1】◆暴萢の概要は,【図10-2】を参照してください.ここは,前回解説しました.



【図10-2 割込み入口処理の概要】

割込み入口処理が終わると,アプリケーションエンジニアが記述した割込みハンドラの処理が実行されます.割込みハンドラからリターンすると,ASPカーネルの割込み出口処理にいくような仕組みが,割込み入口処理にほどこされていました.ですからアプリケーションエンジニアは,この関数は割込みハンドラとして使用しますよ.という宣言をするだけで,通常の関数と同じように割込みハンドラを記述することができます.
今回は,割込みハンドラの処理終了後,ASPカーネルで行われる,割込み出口処理について解説します.【図10-1】では,の部分になります.ここでのポイントは,割込み処理の中で呼ばれたサービスコールによって,ディスパッチが必要となった場合でも,割込みハンドラ処理中には,ディスパッチはおこりません(→遅延ディスパッチ).よって,ディスパッチが必要な場合は,割込み出口処理でディスパッチャに分岐します.一方,ディスパッチが必要でない場合は,RTE命令で割込み先にリターンします.ASPカーネルで割込み出口処理が終了し,RTE命令でリターンすると,SHプロセッサが,SPCをPCに,SSRをSRに復帰し,割り込まれた元の処理に戻っていきます.

今回も,以下の例で,時間の流れにそって処理を見ていきます.



優先度の高いタスクAは,slp_tsk()発行によって起床待ち状態になり,タスクA より優先度の低いタスクBが実行中となっています.そこで割込みが発生し,割込みハンドラから,iwup_tsk(TASKA)によって,タスクAが起こされる場合について見ていきます.割込み出口処理では,この割込みハンドラからの戻り先は,タスクであること,割込みハンドラ中でタスクBからタスクAへのディスパッチが必要となったが保留されていること,を判断し,対応する処理を行います. 今回見ていくのは,【図10-3】で赤丸で囲ったタイミングで行われるASPカーネルの処理です.



【図10-3 割込み出口処理のタイミング】

タスクAはslp_tsk()を発行時に,_dispatchでコンテキストを保存しています(【図10-4】の 法コ箙みハンドラ中で,タスクBからタスクAにタスク切り替えが必要となったので,タスクBのコンテキストを保存して,ディスパッチャコアに入ります(【図10-4】の◆法ゥ妊スパッチ先のタスクAは_dispatchでコンテキストを保存していたので,_dispatch_rでコンテキストを復帰します(【図10-4】の).(第8回で解説)

null

【図10-4 ディスパッチャコアへのルート】
まず,【図10-1】で行う概要を【図10-5】で解説します.【図10-5】は,処理に該当するルーチン名で囲っています.



【図10-5 割込み出口処理概要】

まず,戻り先を判定します.タスクに戻る場合は,スタックポインタをタスク用に切り替えます.そして,タスク切り替えが必要かどうか判断します.
戻り先が割込みの場合と,ディスパッチが必要でない場合は次に,保存したレジスタを復帰します.
ディスパッチが必要な場合には,ディスパッチする準備を行います.必要なレジスタをスタックに積み,SPとPCをTCBに保存します.その後,ディスパッチャコアでタスクを切り替えます.次に,保存したレジスタを復帰するという流れになっています.

割込みハンドラからの戻り方には,3パターンあります.

タイプ‖申迭笋蟾みで,割込み先の割込みハンドラに戻る 
タイプ◆С箙み先ののタスクに戻る
タイプ:ディスパッチが保留されていたので,タスク切り替えが行われ別のタスクに戻る.

【図10-5】に,タイプごとに処理ルートを矢印で示しています.今回の例はタイプになります.この流れのソースコードを解説します.ソースコードが長いので,【図10-5】で,赤い点線で囲った部分ごとにソースコードを切り分けて解説していきます.


 _ret_int>

割込み処理からの戻り先を判定し,タスクに戻る場合は,スタックポインタをタスク用に切り替えます.そして,ディスパッチが必要かどうか判断するという処理を行います.




【図10-6 _ret_int の処理概要】

続いて,ソースコードを読んでいきます
581 _ret_int: 
582 mov.l _mask_md_ipm_ret,r0 /* 割り込み禁止 */
583 ldc r0,sr
584 stc r7_bank,r7 /* 例外/割り込みのネスト回数をデクリメント*/
585 dt r7
586 ldc r7,r7_bank
587 tst r7,r7 /* 戻り先が非タスクコンテキストならすぐに */
588 bf _ret_int_1 /* リターン */
589 mov.l @r15,r15 /* 戻り先がタスクなのでスタックを戻す */
590 mov.l _reqflg_ret,r4 /* reqflgのチェック */
591 mov.l @r4,r1
592 xor r0,r0
593 tst r1,r1 /* reqflgがTRUEならret_int_2へ */
594 bf/s _ret_int_2
595 mov.l r0,@r4 /* reqflgをクリア */


STEP1:戻り先の判定

584 stc r7_bank,r7
585 dt r7
586 ldc r7,r7_bank

584,585行目:BANK1のR7に格納している,例外割込みのネスト回数をデクリメントします.現在は,BANK0なので,BANK1のR7にはr7_bankとアクセスします.デクリメントした値が0なら戻り先はタスク,1以上なら非タスクコンテキストに戻ります.

587 tst r7,r7
588 bf _ret_int_1 

tst Rm,Rn ⇒ Rm & Rn 結果が0のとき,1→T 0でないとき,0→T
bf ⇒ T=0のときジャンプ,T=1のとき,nop(次の行へ)

R7の論理積が0(戻り先がタスク)なら次の行,1以上(戻り先が割込み)なら,_ret_int_1へジャンプします.つまり,現在発生している割込みが多重割り込みで,戻り先が割込みの場合は_ret_int_1へジャンプし,戻り先がタスクの場合は,次の行を実行します.今回は,戻り先はタスクBなので,次の行を実行します.

STEP2:スタックポインタの切り替え

589 mov.l @r15,r15

戻り先がタスクなら,スタックポインタをタスク用に切り替えます.第9回で解説したように,非タスク用スタックに,割込み発生前のタスク用スタックポインタを保存しました.そのスタックポインタを復帰します.【図10-7】



【図10-7 スタックポインタの切り替え】

589行目:@r15とは,現在は非タスク用のコンテキストのスタックポインタを使用しているので,非タスクコンテキスト用のスタックポインタが指しているアドレスの中味,つまり割り込まれたタスク(タスクB)のスタックポインタの値です.この値を,R15に登録することで,スタックがタスク用に切り替わります.

STEP3:タスク切り替えが必要か判定

ここで,非タスクコンテキストから呼び出すことができるサービスコールについて簡単に説明します.たとえば今回割込みハンドラから呼び出しているiwup_tsk()のように,先頭に”i”がついたサービスコールしか非タスクコンテキストから呼び出すことはできません.では,何が違うのかといいますと,例えば非タスクコンテキストは,タスクと違って状態を持ちませんので,待ち状態に入るサービスコールは発行することができないといった違いがあります.このようにタスクから呼びだすサービスコールと,非タスクコンテキストで呼び出すサービスコールを区別しています.今回関係してくる違いは,ディスパッチが必要な場合は,wup_tsk()では,dispatch()を呼びますが,iwup_tsk()では, reqflgをTRUEにするだけです.reqflgはディスパッチが必要かどうかを保持する変数です.詳細は後日解説します.

590 mov.l _reqflg_ret,r4
591 mov.l @r4,r1
592 xor r0,r0
593 tst r1,r1
594 bf/s _ret_int_2
595 mov.l r0,@r4


590,591行目:_reqflg_retはreqflgのアドレスを表します.そのアドレスをR4に格納し,中身をR1に格納します.

592行目:
xor Rm,Rn ⇒ RmとRnのXOR演算結果→Rn

R1どうしのXOR演算をすることで,R0を0にしています.この値は,595行目で,reqflgをクリアするときに使用します.

593,594行目:
tst Rm,Rn ⇒ Rm & Rn 結果が0のとき,1→T 0でないとき,0→T
bf/s ⇒ <遅延分岐(第9回で解説)>T=0のときジャンプ,T=1のとき,nop(次の行へ)


reqflgがTRUE(1),つまり割込み処理中にタスク切り替えが必要となったが,保留されていた場合(ディスパッチ遅延)は,ret_int2へジャンプします.今回の例は,タスクBからタスクAに切り替える必要があるので,reqflgはTRUEであり,ret_int2へジャンプします.

595行目:reqflgを0,つまりクリアしています.

594行目は遅延分岐命令,595行目は遅延スロットルとなりますので,分岐するまえに,595行目のリクエストフラグのクリアが行われます.(遅延分岐命令,遅延スロットルについては,第9回で解説しました.)

<_ret_int_2>
ここに来るのは,タイプ:ディスパッチが保留されていたので,タスク切り替えが行われ別のタスクに戻る場合です.
ここでは切り替えもとのタスクBのコンテキストを保存します.
ここに来るまでに,_interrupt_vecによってタスクBの汎用レジスタのうち,呼び出しもとで保存するコーラーセーブドレジスタであるR7〜R0のみ保存しています.また,スタックポインタは,_ret_intでタスク用のスタックポインタになっています.いまから,ディスパッチを行いますので,ここでは,ディスパッチのための準備を行います.第8回で解説した_dispatch(ディスパッチャ入口処理)と似た処理を行います.

◆保存する汎用レジスタについて
第8回のディスパッチャの入口処理では,コーリーセーブドレジスタ(R8-R13),第9回割込み入り口処理では,コーラーセーブドレジスタ(スクラッチレジスタ R0−R7)を保存しました.どちらでどちらを保存するのかわかりにくいので,もう一度まとめます.
・R0〜R7:呼び出された関数では,破壊してもよいスクラッチレジスタ(コーラーセーブドレジスタ).呼び出し元で保存する
・R8〜R13; 呼び出された側で保存(コーリーセーブドレジスタ)



【図10-8 呼び出し関係概要】

ディスパッチの場合は,ASPカーネル中のサービスコールの処理でディスパッチが必要になった時に,dispatch();という関数呼び出しによって,ディスパッチ入口処理(_dispatch)に到達します【図10-8】.ですからここでは,呼び出された側で保存するコーリーセーブドレジスタ(R8-R13)を保存します.
一方,割込みの場合は,ASPカーネルの割込み入り口処理(_interrupt_vec)では,これから割り込みハンドラである関数を呼び出します【図10-8】.ですから,割込み入口処理では呼び出し元で保存するコーラーセーブドレジスタ(R0-R7)を保存します.


以上を頭に入れた上で,_ret_int2の概要を見ていただきます.

STEP1:CPUロック状態へ移行
STEP2:ディスパッチが必要でない場合の処理
STEP3:コーリーセーブドレジスタの保存
STEP4:SPをTCBに保存
STEP5:実行再開番地(_ret_int_r)をPCとしてTCBに保存
STEP6:dispatcherへジャンプ

それでは,ソースコードを読んでいきます.

626 _ret_int_2:
643 mov.l _lock_flag_ret, r2 /* lock_flagをTRUEに */
644 mov #0x01,r1
645 mov.l r1,@r2
646 mov r15,r0
647 mov.l @(11*4,r0),r2 /* 割込み前のIPMを取り出して */
648 mov r2,r0
649 and #0xf0,r0 /* saved_iipmに */
650 mov.l _saved_iipm_ret, r1
651 mov.b r0,@r1
658 mov.l _p_runtsk_ret,r1 /* r0 <- p_runtsk */
659 mov.l @r1,r0
660 mov.l _dspflg_ret,r2 /* dspflgがFALSEならret_int_3へ */
661 mov.l @r2,r3
662 tst r3,r3
663 bt _ret_int_3
664 mov.l _p_schedtsk_ret,r4 /* r5 <- schedtsk */
665 mov.l @r4,r5
666 cmp/eq r0,r5 /* runtsk と schedtsk を比較 */
667 bt _ret_int_3 /* 同じならret_int_3へ */
668 mov.l r14,@-r15 /* 残りのレジスタを保存 */
669 mov.l r13,@-r15
670 mov.l r12,@-r15
671 mov.l r11,@-r15
672 mov.l r10,@-r15
673 mov.l r9,@-r15
674 mov.l r8,@-r15
675 mov r15,@(TCB_sp,r0)
676 mov.l _ret_int_r_ret,r1 /* 実行再開番地を保存 */
677 bra _dispatcher
678 mov.l r1,@(TCB_pc,r0)


STEP1:CPUロック状態へ移行
643〜651行目:dispatcherに移行する前には,CPUロック状態にしておく必要があるため,その処理を行います.詳細の解説は省略します.

STEP2:ディスパッチが必要でない場合の処理
658,659行目:p_runtskの中身,つまり現在実行中のタスク(タスクB)のTCBアドレスが,R0に格納されます.

660〜667行目
この_ret_int2には,タスク切り替えが必要な場合と,タスク例外処理ルーチンの呼び出しが必要な場合もreqflgをTRUEにしてここにきます.タスク例外処理ルーチンの呼び出しの場合は,ディスパッチは必要ないのでそのための処理を行います.詳細の解説は省略します.

STEP3:コーリーセーブドレジスタの保存
ここに来るまでに_interrput_vecで,コーラーセーブドレジスタのみ保存されています.これから,ディスパッチを行いますので,まだ保存していないタスクBのコーリーセーブドレジスタ(R8〜R14)を保存します.
668〜674行目:R14からR8の順で,スタックに保存します.

STEP4:SPをTCBに保存
R15(スタックポインタ)を,TCB(タスクB)に格納します.
675行目:TCB_spはTCB中のsp保管場所のオフセットです.R0は切り替え元のタスク(タスクB)のTCBアドレスでした(658,659行目).現在のスタックポインタを,タスクBのTCBに保存します.

STEP5:実行再開番地(_ret_int_r)をPCとしてTCB(タスクB)に保存
676行目:_ret_int_r_retは,_ret_int_rのアドレスを表します.
678行目:TCB_pcは,TCB中のpc保管場所のオフセットです._ret_int_rをTCB(タスクB)に格納します.(第8回の例では,_dispatch_rを格納しました.)
この処理によって,次にタスクBが実行状態となり,コンテキストを復帰するときは,_ret_int_rで復帰します.

STEP6:_dispatcherへジャンプ
676行目:_dispatcher(ディスパッチャコア)にジャンプします.ディスパチャコア内での処理に関しては,第8回を参照してください.ディスパッチャコアでは,以下のような処理を行います.

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

つまり,p_runtskは,タスクAのTCBアドレスを指しており,スタックポインタはタスクAのスタックポインタに切り替わります.
タスクAは,_dispatchでコンテキストを保存しているので,_dispatch_rでコンテキストを復帰します.(【図10-4】の)



次回は,今回の例では通らなかったルートのソースコード,_ret_int_1,_ret_int_rの解説を行います.そして,割込み処理が発生したときに,何をいつ保存して,復帰するのかわかりづらいので,全体をまとめてみたいと思います.