本投稿、[新卒が作る自作OS]は、我々が自作OSを作るにあたり、詰まったところや、備忘録的に残しておきたいところなどをまとめておこうという趣旨の投稿です。
今回の投稿はOS開発には不可欠な割り込み処理と例外についてまとめました。
割り込み
割り込みとは
ディスプレイやキーボードなどのI/O処理は、プロセッサで完結する演算などの処理に比べて非常に時間がかかります。I/O処理の完了を待っている間、プロセッサのリソースを遊ばせておくのは勿体ないです。そのためI/O処理を待っている間は別の処理を行いI/O処理が終わったら続きを行うようにするのが効率的です。
I/O処理の続きを再開するにはI/O処理の終了を検知する仕組みが必要です。これが割り込み(Interrupt)です。I/O処理の動作が完了したら、I/O処理を制御するI/Oコントローラはプロセッサに完了通知を行います。これにより、プロセッサは待っているだけでI/O処理の完了を把握することができます。
割り込みの仕組み
割り込みは以下のような手順で行われます。
- 割り込み入力線から割り込みを確認
- 実行中のプログラムとレジスタを退避
- 割り込み時に実行する命令を読み出す
- 割り込み時に実行する命令を実行
- 退避したプログラムとレジスタを読み出す
- 退避した命令を実行
割り込み処理の受付は割り込み入力線から行われます。割り込み入力線に信号が送られると以降の割り込み処理が実行されます。
まず最初に行うのは現在実行中の命令を中断し、次に行うべき命令と使用中のレジスタの値を退避用のレジスタに保管します。退避用レジスタには次に行う命令を保存する「プログラムカウンタ保存用レジスタ」と使用中のレジスタの値を保存する「汎用レジスタ保存用レジスタ」があります。
I/Oを使用するプログラムはI/Oコントローラに制御を移した時点で「割り込み処理命令アドレスレジスタ」にI/O処理後に行う命令を保存しておきます。現在実行中の命令の退避が終わったら、割り込み処理命令アドレスレジスタに保存されている命令を読み出し、その命令を実行します。
割り込み命令が終わると、退避していた命令を復元し再開します。プログラムカウンタ保存用レジスタと汎用レジスタ保存用レジスタから値を読み出し、プログラムカウンタレジスタと汎用レジスタに書き込み、中断していた処理を実行します。
下図を元に例を見てみましょう。プログラムBでI/O処理を行う命令B1が実行されました。これによりプロセッサは割り込み処理命令アドレスレジスタにI/O終了後に実行する命令B2のプログラムカウンタを書き込み、プログラムBはI/Oの終了まで待機します。ます。その間にプロセッサはプログラムAを実行します。命令A1を実行した直後にI/O処理が完了し、割り込みが発生しました。プログラムカウンタと汎用レジスタを退避します。プログラムカウンタ退避用レジスタには割り込み終了後実行すべき命令A2のプログラムカウンタを、汎用レジスタ退避用レジスタにはプログラムAで使用中のレジスタを退避します。その後、割り込み処理命令アドレスレジスタに書き込まれているプログラムカウンタを確認し、次に実行する命令が命令B2だとわかるのでこれを実行します。プログラムBが終了したら、退避用レジスタの値を読み込み、次に実行する命令が命令A2だとわかるので、実行を再開します。
例外
例外とは
割り込みはハードウェアの処理を待つための仕組みですが、これをソフトウェアの処理に応用したものが例外(exception)です。トラップ(trap)とも呼びます。プログラムを実行していると、メモリのアクセス違反やゼロ除算など、そのままではプログラムの実行を継続できなくなる場合があります。このような例外的な事象に対応するための仕組みが例外です。
例外の仕組み
例外は割り込みを元にしているので仕組みはほとんど同じです。プログラムの実行を継続できなくなったときに割り込みを行います。割り込み時に行う処理として、発生したエラーを取り除きプログラムの終了もしくは再開などを実行します。
例外が発生しそうな処理(C言語におけるtry{}中の処理)が割り込みにおける現在実行中の処理、例外時に行う処理(C言語におけるcatch{}中の処理)が割り込み発生時に行う処理に対応します。
また、ソフトウェアでI/Oを使いたいときは、ソフトウェア割り込みという特別な例外を用いてOSに依頼します。
ベクタ割り込み
ベクタ割り込みとは
ここまでは、割り込みを行う入出力装置が一つである前提で考えました。しかし、実際にはモニタ、スピーカ、マウス、キーボードなど複数の入出力装置が接続同時に使われることがほとんどです。この場合、割り込みが起こったときにその原因を特定するために、すべての装置のステータスレジスタを確認しなければならず、処理に時間がかかります。また、割り込み処理の実行中に別の割り込みを許可するかを考えなくてはなりません。装置の処理速度によって、素早く処理しなければオーバーランしてしまうものや、緊急性の低いものなどを考慮し割り込みをコントロールする必要があります。これらの問題を解決するための仕組みがベクタ割り込みです。
ベクタ割り込みの仕組み
ベクタ割り込みでは、割り込みの優先度を表す割り込みレベルを指定します。割り込みレベルは0が最高で1、2…と値が大きくなるごとに緊急度が低いものを割り当てます。また、割り当てる割り込みの数は優先度が高いものほど少なくなるように指定します。これは優先度の高い処理を高速に行うためです。
複数の割り込みが連続して発生したとき、発生した割り込み処理の割り込みレベルが現在実行中の割り込み処理の割り込みレベルより低いときは割り込みが禁止されます。このようにして、割り込み処理に優先順位を設定しています。
退避用のレジスタの数
割り込み処理中に別の割り込み処理を行うと、退避用のレジスタは既に使用しているのでそのままでは使えません。これを解決するために、プロセッサには2つの方式があります。
一つは退避用レジスタを複数用意する方式です。LIFO式のレジスタスタックを用意することで多重割り込みを実現しているプロセッサもあります。
退避用レジスタスタックを持たないプロセッサの場合はメモリに退避します。多重割り込み時、命令の初めに退避用レジスタの値をメモリに退避します。こうすることで退避用レジスタが空くので更に割り込みすることができます。この処理を行う間は割り込みを禁止しておく必要があります。ただし、この方法はレジスタスタックに比べ処理時間がかかります。
ポーリング
最後に補足として、I/O処理の終了を検知する割り込み以外の方法としてポーリングを説明します。
I/O処理の動作状況はI/Oコントローラ内のステータスレジスタに書き込まれています。これを参照することでI/O処理の完了を確認することができます。定期的にステータスレジスタを読み込みI/Oの終了を検知できます。
ポーリングで高速な検知を行おうとすると、頻繁にステータスレジスタを参照する必要があります。そのためプロセッサに高い負荷がかかります。多くのプロセッサではプロセッサの負荷軽減のため割り込みが用いられますが、レジスタの退避などが不要なため、どうしても高速な応答確認が必要な場合はポーリングが用いられます。
まとめ
この記事では割り込みと例外に関連する処理についてまとめました。どちらもOS開発には欠かせない要素です。使い所が異なるので意識することはあまりありませんが、これらは同じ仕組みで動作しています。
最後までお読みいただきありがとうございます。