読者です 読者をやめる 読者になる 読者になる

割り込み処理〜コンテキストスイッチ

かなり簡単な方法ですが、コンテキストスイッチに成功しました。(リビジョン6)
深い部分に手を入れたため、知識不足による大いなる勘違いや、確認方法の間違いなどで根本的におかしいかもしれません…。


今回移植したモジュールは、

  • KObject
  • Process
  • Scheduler

です。これらのモジュールに依存するコードは無効にしていましたが、今回の移植によって可能な限り有効にしました。

但し、必要最小限のコンテキスト切替の確認に重点を置いたため、今回も一部不完全で以下のようになっています。

  • ThreadOperation::switchThread関数(Process.cpp)
    • プロセスコンテナの変更の判断、及びKernel/Userプロセスの判断は無効。
  • Scheduler::SetNextThread関数(Scheduler.cpp)
    • runqからの次スレッドの線形探索を無効。
      • 2つのスレッドを1秒毎に交互に切り替わるように。
  • irqHandler_0関数(タイマ割り込みハンドラ:ihandlres.cpp)
    • Scheduleを無効。
      • 無条件にSetNextThreadを実行するように。
  • startKernel関数(kernel.cpp)
    • 引き続きMutex作成を無効。
      • syscall/Mutexの移植が未実施のため。

移植概要

オリジナルの割り込み発生からハンドラ処理までの大まかな流れは以下の様になっています。

  • 予め行うIDTの初期化で、handlers配列をIDTRに登録
    • handlersはhandler.asmに記述された割り込み・例外ハンドラのテーブル
  • irq発生でvector番号に対応したhandlers配列のハンドラに飛ぶ
  • アセンブリからCのハンドラ本体(handlers.cpp)を呼ぶ

これに対して、基本的な流れはそのままに以下の様な構成としました。

  • vectorテーブルのirqのオフセットに割り込みハンドラへのブランチ命令を記述(astart.S)
  • irq発生でvectorテーブルの当該のハンドラに飛ぶ
  • アセンブリからCの割り込み処理共通関数(handlers.cpp)を呼ぶ
  • 割り込みに対応したハンドラを実行

x86とは異なり、ARMではirqvectorが1本に丸められるので、共通の割り込み処理で発生した割り込みソースを確認した後、対応したハンドラ本体を実行します。
また、速度面からcommon_isrは廃止予定で、オリジナル同様に割り込み要因番号に対応したハンドラテーブルを準備して、アセンブリから直接飛べるようにしたいと思います。

コンテキストスイッチ概要

まずはオリジナルです。インターバルタイマの割り込みが発生して(必要であれば)コンテキスト切り替えが行われるまでの流れを追ってみます。

割り込みが発生するとDPL>=CPL*2となって、予めTSSに登録しておいたスタックに切り替わります。そしてハンドラにてスタック上にコンテキストを保存した後、g_CurrentThreadが指し示すThread構造体に格納します。

その後、必要であればコンテキストスイッチが発生して、g_CurrentThreadが切り替わり、その先のコンテキストをCPUとスタック上に復元して割り込み処理から復帰します。

上記はコンテキストスイッチに的を絞って処理を追っているため、実際にはCPLの確認や、切り替え先のCPLの違いによるコンテキストの復元の仕方が若干異なる部分は省略しています。

上記の流れを以下のように移植しました。

まず、ARMでは計7つものCPUモードがあります。

モード 用途 特権 例外
usr ユーザ向け × ×
sys OS向け ×
svc ソフトウェア割り込み発生時
abt プリフェッチアボートまたはデータアボート発生時
udf 未定義命令実行時
irq irq発生時
fiq fiq発生時

CPUモードはcpsrのbit0:4で表現され、例外モードは当該の例外が発生した際にCPUがcpsrを変更します。
特権モードであればcpsr(ステータスレジスタ)の設定によって各モードを自由に行き来する事が可能です。
また例外モードでは、それぞれに独立した以下のレジスタ(バンクレジスタ)が設けられています。*3

レジスタ バンク 使われ方
r0〜r12 fiqモードのみr8〜r12がバンクされる abiによってまちまち
r13 sp
r14 リンクレジスタ(lr)。サブルーチンコール呼出時、pc+4がCPUによってセット
r15 - pc
cpsr - ステータスレジスタ
spsr 例外モードのみ。例外発生時のcpsrの値が格納される

lrは例外時、例外が発生したPC+4が格納されます*4


バンクレジスタは例外発生時に、CPUが自動的に切り替えてくれる寸法です。
但し、spは前回そのモードであったときの値が復元されるので、予め初期化しておく必要があります。
従って、予め明示的に例外モードになってバンクレジスタを設定しておけば例外発生時に切り替わります。
spにおいてはまさに、x86のTSSのようなメカニズムです。

CPUモードとバンクレジスタを踏まえて、上図のレジスタをスタックに積む処理(astart.S)を細かく補足すると、

  1. irq発生(CPUモードがirqモードに)
  2. r0からr5をスタック(irqモード)に退避(r0-r5を一時的に利用するため)
    • spの位置は変更しない
  3. r3にr14から4を引いた値(割り込み発生時のpc)を格納
  4. r4にspcr(割り込み発生時のcpsr)を格納
  5. r5に(irqモードでバンクされた)spを格納
  6. 現在のプロセッサモードを割り込みが発生時のモード(4.のspcr)に変更
  7. r1にspを保存(割り込み発生時のsp)
  8. r2にlrを保存(割り込み発生時のlr)
  9. 現在のプロセッサモードをsvcモードに変更
  10. r1からr4までをスタック(svc)に保存
  11. (irqモードのspである5.で取得した)r5の内容のアドレスから、5レジスタ分(1.で退避した)をr0からr5に復帰
  12. スタック(svc)に割り込み発生時のバンク以外のレジスタr0からr12までを退避
  13. コンテキスト保存処理へ

という手順になります。

動作確認

冒頭でも記載したとおり、必要最小限のコンテキスト切替までの移植としています。
具体的には、

  • 2つの特権モード(sysモード)のスレッドを生成
    • 1つ目のスレッドは1秒毎にメッセージ表示。その間はビジーループ。(既存のmainProcessを修正)
    • 2つ目のスレッドは3秒毎にメッセージ表示。その間はビジーループ。(既存のmonaIdleを修正)
  • 1秒毎に交互にコンテキストスイッチ

として、確認しました。
ただ、一点問題があり、本当のビジーループだとコンテキストスイッチしないため、ループ中にシリアル出力(文字は無し)を行っています*5。原因解析中…。

実行すると2つのスレッドが切り替わるのが確認できます。

% ./runqemu
Mona version.0.3.0Alpha9 on ARM920T(S3C2440A/Bishop) $Date:: 2008-04-29 11:35:00 +0900#$
Setting PIC        [OK]
System Total Memory 64[MB]. Paging on
mainProcess[0]
mainProcess[1]
monaIdle[0]
mainProcess[2]
mainProcess[3]
mainProcess[4]
mainProcess[5]
monaIdle[1]
mainProcess[6]
mainProcess[7]
monaIdle[2]
mainProcess[8]
mainProcess[9]
mainProcess[10]

気になった点

オリジナルでは、コンテキストスイッチが発生する際、iretはコンテキスト復元処理の仕上げに行っています(場所はarch_switch_threadx)。
移植もこれに沿っていますが、個人的には、iretは割り込みハンドラからの復帰で一本化し、iretの点在は避けるべきと考えます…。
x86に詳しくないので背景が不明ですが、そうせざるを得ない理由が無い限り、Monarmでは、割り込み復帰でコンテキストが切り替わるように変更したいと思います。

長文で

大げさにしていますが、あまり大幅に進んでいないような…
次は、Process/Schedulerを仕上げたいと思います。

*1:または0xFFFF0000に設定可能

*2:値ではなく意味として

*3:逆に、例外以外のモードはバンクされない

*4:プリフェッチアドレス

*5:nop4発でもNG