UNIX 6th code reading - 割り込み・トラップルーチン

初めての方はこちらを参照 http://d.hatena.ne.jp/takahirox/20101024/1287923014

はじめに

今回は10章です。

トラップと割り込みルーチンを追っていきます。各トラップ・割り込みの個別処理は次章以降で扱われます。本章では個別処理に入る前・入った後の共通処理部分を見ていきます。

何故、割り込み・トラップという機構があるのか

割り込みの説明

割り込み・トラップのロジックを追う前に、その機構が存在する理由を自分なりに考えてみました。

割り込み機構は絵にするとこんな感じです。

各デバイスはCPUに対し要求をするときにREADY/DONE bitのフラグ(CPUのstate bit7?)をセットします。

また、CPUは1loop毎にDONE/READY bitを調べます。セットされていたら割り込み処理を、セットされていなければ通常処理を行います。


追記 : 上記は実際のPDP11の機構とは著しく異なります。PDP11の割り込み機構についてまとめたエントリを別に作成したので詳細はそちらで確認してください。


この方式の優位点は以下です。

1. loop毎に、いちいち全てのデバイスをチェックしなくてもOK(高速化、資源の有効活用)

バイスからの要求頻度はCPUの実行速度に比べるときわめて遅いです。もしCPUが1loop毎に全てのデバイスをチェックするとしたら、滅多に起きないイベントのために無駄にCPUリソースを消費することになります。

2. デバイスの追加・削除が行いやすい(柔軟性)

バイスが割り込むためには前述の通りDONE/READY bitをセットすればいいです。つまり、このDONE/READY bitがデバイスに対するインターフェイスになっているといえます。

なので、新たにデバイスを追加したり、既存のデバイスを削除したり、ということが容易にできると思われます。

この機構がなければ、CPU(OS?)と周辺デバイスに密な繋がりができてしまい、問題を切り離して考えることが難しくなります。

では、トラップでは?

割り込みと同じようなメカニズムを用いているトラップにも同様のメリットがあると言えるでしょう。

トラップはわり算における0割など例外的な動作に対し起こります。

もし、トラップという機構がなければ、例えばわり算を行った後は必ず計算成否フラグを調べて失敗していたら分岐するといった処理が必要になります。

しかし、滅多に起こらないであろう例外動作のために毎回フラグチェック処理を行うのは無駄が多いです。

そのため、例外的な動作が起こった場合にのみトラップにより処理を割り込ませるという方式が取られているのだと思います。

また、割り込みと同じように、例外動作を追加することも容易なので柔軟性が高いと言えるでしょう。

Lions本2章ではPSのbit4がトラップビットとなっています。CPUはこれをチェックしてトラップを捕まえると思われます(?? 要確認)

トラップ・割り込み時のスタックの状態

トラップ・割り込み時のスタックの状態がLions本に書かれていなかったので、PDP11 handbookで調べました。ここに紹介しておきます。

割り込み・トラップ時

実行中のプロセスのPS, PCをスタックに積みます。そしてPS, PCにはvector addressより決定される新しい値が入ります。

sp stack
 
sp -> PC (return to interrupted process)
PS
 
register value
PC <- something value from vector address
PS <- something value from vector address

新しいPS, PCの値は500-549行目あたりで確認できます。

例えばバスタイムアウトトラップ(Vector address = 4)の場合は、512行目の1word目のtrapがPCにセットされ、2word目のbr7+0.がPSにセットされます。ただしPSの上位bit([15:12])の値がどうなるかはよくわかりません。値が変化しないか、ハードウェアによって制御されているかどちらかだと思います。

507行目の. = 0^.はここがアドレス0であると設定しています。520, 525行目なども同じ要領です。それらの行とVector addressとをにらめっこすれば、どこの値がPS, PCにセットされるか理解できると思います。

割り込み・トラップからのリターン時

アセンブリ命令のrtt命令で割り込み・トラップから復帰するようです。

rtt命令が実行されると、スタックの最上段からPC, PSを復帰させます。復帰したPCがリターンアドレスとなります。

sp stack register
 
PC (return to interrupted process) -> PC
PS -> PS
sp ->  

割り込み・トラップ処理の流れ

Lions本では三つの割り込み・例外処理の例が書かれています。

fuiword

fuiword内のmfpi命令でバスタイムアウトが発生したときのことを考えます。

トラップ時の処理の流れはこんな感じになります。


ポイントは以下だと思います。

  • trapが起こる前の処理
    • トラップが発生しうる命令(mfpi)の前にnofaultを設定
    • nofaultにはトラップ時に処理を行う命令のアドレス(err)が入っている
  • trapでの処理
    • nofaultをチェック
    • trapからの戻りアドレスをnofault(err)に設定
    • rtt命令でトラップ処理命令(err)にリターン


トラップ発生有無の流れをまとめて絵にするとこんな感じになります。

クロック割り込み

(クロック)割り込みがmfpiのバスタイムアウトトラップと大きく異なる点は、実行中のプロセスがどのタイミングで割り込まれるか知らないという点だと思います。

クロック割り込みの処理の流れを絵にするとこんな感じです。

割り込みの特徴は、割り込まれたプロセスがユーザーモードで実行されていた場合割り込まれたプロセスに戻らないことが有りうる点だと思います。

割り込み処理完了後runrun(実行中のプロセスより優先度の高いプロセスが存在することを示すフラグ)をチェックして、フラグがたっていた場合はswtch( )を呼び出して実行プロセスを切り替えます。

なお、

jsr r0, hoge; fuga

は現在のr0をスタックに積んで、r0 = fugaのアドレス, PC = hogeのアドレス(hogeへのジャンプ)となります。

sp stack
 
sp -> r0
 ...
register value
PC <- address to hoge (this means jump to hoge)
r0 <- address to fuga

なので

570 : jsr r0, call; _clock
.
.
.
785 or 799 : jsr pc, *(r0)+

でclock(3725)に飛べるのです。

ユーザープログラムのトラップ(システムコール)

絵にするとこんな感じです。

最初はバスタイムエラートラップと同じ処理ですが、nofaultが設定されていないので割り込みと同じパスを辿り、r0経由で_trap(2693)にジャンプします。

trapとcallの違い

Lions本読書会#4にて「trapとcallって結局何が違うの?」という話題があがりました。

その場でも説明しましたが、自分の理解をここに記しておきます。

trapでnofaultをチェックして、nofaultが設定されていれば、そのtrap handlerに飛び、設定されていなければcallと同じパスを辿ります。

Cのハンドラに飛ぶオーバーヘッドを気にする場合や、共通ハンドラによる処理をさせるまでもない処理を行いたいときや、割り込み・トラップ処理の中でも例外的なことを行わせたいときにnofaultを設定するのだと思います。

ただし、nofaultを事前に設定できるのはカーネルモードで動いているときだけなことに注意。

その他メモ

  • 割り込まれたプロセスがユーザーモードで実行されていた場合、swtchを呼び出して切り替えるのは何故だろう?
    • このタイミングで優先度の高いプロセスに切り替えちゃえ、ってこと?
  • 割り込み・トラップでSSRを再設定するのはなぜ?

終わりに

割り込み・トラップというのは「滅多に起こらないイベントを如何に効率よく処理するか」がポイントだと思います。
「滅多に起こらないイベント」というのはどのレイヤでもある話なので、例えばアプリ開発においてもこの設計の思想を用いることができるのではないでしょうか。

次回は11章、もしくは10章のコード詳細メモを書く予定です。