CSAPP 第8章 例外的な制御フロー
プロセッサがイベントの発生を検出すると、例外テーブルと呼ばれるジャンプテーブルを介して、プロシージャ呼び出しが発生する。
このときに呼ばれるのはOSのサブルーチンである、例外ハンドラとなる。
例外ハンドラは3つの制御のうち一つを行う。
- 現在の命令位置に戻す。
- 次の命令位置に戻る。
- 元のプログラムを中断させる
例外には例外番号というものが割り当てられている。
例外が発生した際に、例外番号に相当するジャンプテーブルのインデクスを参照して、そのアドレスが指すプログラムをよびだす。
基本的に例外処理はカーネルで実装されるため、制御はカーネルに移譲し
スタックもカーネルスタックを使用したプログラムになる
例外は4種類に判別される
- 割込: interrupt
- トラップ: trap
- フォールト: fault
- アボート: abort
割込
プロセッサ外のI/Oデバイスから非同期に発生する。
ネットワークアダプタ、ディスクコントローラー、タイマーなどが該当する。
ハードウェアからチップのピンに対して、割込のシグナルを送ると同時にバスに例外番号を送信する。
これにより、割込を発生させたデバイスを識別する。
プロセッサは実行中の命令が終了した後、バスから割込命令を読み出し適切な割込ハンドラを読み出す。
トラップ
意図的な例外。
トラップハンドラは次の命令に制御を戻す。
トラップの例として、システムコールがある。
システムコールはユーザープログラム側でカーネルのプログラムを呼び出す橋渡し役
カーネルのプログラムはハックされるとやばいプログラムが多い。
そのためメモリ空間やシステムの呼び出しなどは、セーフティに行われる。
フォールト
回復の可能性があるエラー
例外が発生すると制御をフォールトハンドラに移す、もし回復できそうな場合は元の命令に制御を戻して再実行をする。
もし回復が無理だと判断した場合はabortルーチンに制御を移動して、プログラムを強制終了させる。
回復できる処理の典型例として、ページフォールトが存在する。
これは仮想アドレスを参照した際に、それに対応したメモリが存在しない場合に起こる。
対応したページのデータをディスクから持ってくる。
アボート
回復不能エラー。制御が元のプログラムに戻ってくることはない。
abort handler → abort routineの順番で呼び出される。
フォールトの例として
- 0除算
- 一般保護フォールト
プログラムが仮想メモリ上の未定義領域を参照した場合 - ページフォールト
メモリページが存在しない時のフォールト
フォールトは回復可能としたが、除算エラーなどはUnix系では回復はせずにプログラムをアボートする
この辺の処理は、結局アボートしているのか処理が回復しているどっちなんでしょう
Linuxのシステムコールにおける引数は全てレジスタに保存される。
%rax : システムコール番号
%rdi, %rsi, %rdx, %r10, %r8, %r9 : これらによって引数が渡される。
32bitと64bit命令ではレジスタや命令が異なる
プロセス
複数のプログラムを同時に実行すること
これを実現するために
- 一つのプログラムがプロセッサを専有して見えるように他のプロセスから独立した論理的制御フローを提供する
- 一つのプログラムがメモリを専有して見えるように、各プロセスに専用のアドレス空間を提供する
タイムスライス(マルチタスク)
プロセスを非常に短い時間で区切って複数プロセスを同時に実行させる技術
プロセッサは通常モード・ビット呼ばれるビットをコントロール・レジスタにもっている。
このビットはプロセスの特権を表すビットで、モード・ビットがセットされている場合、カーネルモード(スーパーバイザーモード)で全てのメモリ、命令セットに対して実行権限を持つ。
逆にモードビットがセットされていない場合、プロセスはユーザーモードで実行できる。
このモードでは特権命令は実行できない。
ユーザープログラムがカーネル領域・コードにアクセスするためには、システムコールを使う必要がある。
またカーネルモードにモード変更をするのは例外よってのみ成り立つ。
→じゃあシステムコールによってカーネルにアクセスするときは、モードは切り替わっていないということなのだろうか?
トラップはそもそもシステムコールも含まれている
コンテキストスイッチ
カーネルはそれぞれのプロセスごとにコンテキストを管理している。
コンテキストは 汎用レジスタ、PC、ユーザースタック、カーネルスタックなどの様々なデータ構造を管理している。
マルチタスクを実行するには、現在のプロセスをプリエンプトし、既にプリエンプトされてある、プロセスをスケジューリングによって再開する。
これはカーネルのスケジューラーと呼ばれるプログラムによって実現されている。
fork
子プロセスを作ることができるコマンド。
子プロセスはコードセグメント、データセグメント、ヒープ、共有ライブラリ、ユーザースタックがコピーされる。
ファイルディスクリプタもコピーされる。
なのでプライベートアドレス空間がディープコピーされている感じ
プロセスを作成した後に回収しなかった場合、そのプロセスのために生成されたプライベートアドレス空間は残ったままになる。
そのプロセスをゾンビという。
ゾンビプロセスは通常initプロセスによって回収される。
initプロセスはシステムの初期化時にカーネルによって作成される。
initプロセスのpidは1、プロセスが終了することはなく、全てのプロセスの先祖に当たる位置に存在する。
waitpid
子プロセスの終了を待つ関数
シグナル
プロセスやカーネルが他のプロセスに対して、割込を行うことができる信号。
シグナルは番号によって管理され、それぞれ送信するステータスが異なる。
具体例
- SIGSEGV
セグメント違反 - SIGINT
キーボードからの割込
送信された側のプロセスは、ユーザーレベル関数のシグナルハンドラをつかってシグナルを受け取る。
シグナルは複数受け取る事ができるが、最初に送られてきたもの以外は単純に捨てられる。
→ペンディングシグナルという
プロセスグループ
プロセスはpidの他にプロセスグループというものを持つ。
子プロセスと親プロセスは同じプロセスグループに属する。
killコマンド
/bin/kill
を使うことで、任意のシグナルをプロセスに送信することができる。
-9: SIGKILL プログラムの強制終了
キーボードからCtrl + C
を入力すると、カーネルはフォアグラウンドのプロセス・グループに属する、全てのプロセスにSIGINTシグナルを送信する。
その結果全てのフォアグラウンドプロセスは終了する
Ctrl + Z
はサスペンドする
シグナルセーフ関数
シグナルハンドラの実装はトリッキーで扱いづらい。
そのためバグを発生しやすくしてしまう。
そこで非同期シグナルセーフ関数を使用することで、バグになりにくい関数を作る。
なにがどう安全なのかは後の章で見ていく。
setjump, longjump
ユーザーモードで例外処理を行うことができる関数