CPU とメモリの仮想化の必要性#
メモリの仮想化により、アプリケーションプログラミングが簡単になり、オペレーティングシステムはプログラムに大きな独立したアドレス空間を提供し、実際の小さく混雑した物理メモリの割り当て方法を考慮する必要がありません。また、他のプログラムのメモリが悪意のあるプログラムによって変更されないように保護することもできます。
CPU の仮想化により、オペレーティングシステムはスケジューリングアルゴリズムを使用して、複数のプログラムが実行されている場合でも、CPU が単独のプログラムに占有されないように保証することができます。
仮想メモリの 3 つの目標:透明性、効率性、保護性。
ゾンビ状態#
プロセスは終了したがまだクリーンアップされていない最終状態(UNIX ベースのシステムではゾンビ状態と呼ばれる)になることがあります。この最終状態は非常に便利であり、他のプロセス(通常は親プロセス)がプロセスのリターンコードをチェックし、直前に完了したプロセスが成功したかどうかを確認できます(通常、UNIX ベースのシステムでは、プロセスがタスクを正常に完了した場合はゼロを返し、それ以外の場合はゼロ以外を返します)。
シェルの呼び出しプロセス、fork、exec#
シェルもユーザープログラムであり、まずプロンプトを表示し(prompt)、ユーザーの入力を待ちます。それに対して、コマンド(実行可能プログラムの名前と必要なパラメータ)を入力できます。ほとんどの場合、シェルはファイルシステムでその実行可能プログラムを見つけることができ、fork()を呼び出して新しいプロセスを作成し、exec()のいずれかのバリエーションを呼び出してその実行可能プログラムを実行し、wait()を呼び出してそのコマンドの完了を待ちます。子プロセスの実行が終了すると、シェルは wait()から戻り、再びプロンプトを出力し、次のコマンドの入力を待ちます。
fork()と exec()の分離により、シェルは便利な機能を簡単に実装できます。例えば:
wc p3.c > newfile.txt
上記の例では、wc の出力結果がファイル newfile.txt にリダイレクトされます(リダイレクトを指定するための大なり記号が newfile.txt の前にあります)。シェルは結果のリダイレクトを実装する方法も非常に簡単で、子プロセスの作成が完了した後、シェルは exec()を呼び出す前に標準出力(standard output)を閉じ、ファイル newfile.txt を開きます。これにより、実行されるプログラム wc の出力結果がファイルに送信され、画面に表示されなくなります。
CPU 仮想化の重要な基礎メカニズム、制限付き直接実行#
システムコールのプロセス、トラップ命令、トラップテーブル#
システムコールを実行するには、プログラムは特別なトラップ(trap)命令を実行する必要があります。この命令は同時にカーネルにジャンプし、特権レベルをカーネルモードに引き上げます。一度カーネルに入ると、システムは必要な特権操作(許可されている場合)を実行し、呼び出しプロセスが必要な作業を実行できるようにします。完了後、オペレーティングシステムは特別なトラップから戻るための特別な命令を呼び出します。通常どおり、この命令は呼び出し元のユーザープログラムに戻り、特権レベルを下げてユーザーモードに戻ります。
まだ議論していない重要な詳細があります:トラップが OS 内で実行するコードをどのように知るのか?
明らかに、呼び出しプロセスはジャンプ先のアドレスを指定することはできません(プロシージャ呼び出しを行う場合と同様に)。これを行うと、プログラムがカーネル内で任意の場所にジャンプできるようになり、これは明らかに悪いアイデアです。実際、この機能を使用すると、巧妙なプログラマがカーネルで任意のコードシーケンスを実行できる可能性があります。したがって、カーネルはトラップが実行されるコードを慎重に制御する必要があります。
カーネルはトラップテーブル(trap table)を設定することでこれを実現します。マシンが起動すると、特権(カーネル)モードで実行されるため、必要に応じてマシンハードウェアを自由に設定できます。オペレーティングシステムが最初に行うことは、ハードウェアにこれらのトラップハンドラの場所を通知する特別な命令を使用することです。
たとえば、ハードディスクの割り込み、キーボードの割り込み、またはプログラムのシステムコールが発生した場合、どのコードを実行する必要がありますか?オペレーティングシステムは通常、特別な命令を使用してハードウェアにこれらのトラップハンドラの場所を通知します。ハードウェアが通知されると、これらのハンドラの場所を覚えており、次回のマシンの再起動まで保持されます。ハードウェアはシステムコールや他の例外イベントが発生した場合に何をすべきか(つまり、どのコードにジャンプするか)を知っています。
最後に、もう一つ挿入します:ハードウェアにトラップテーブルの場所を指示する命令を実行できる能力は非常に強力な機能です。したがって、おそらく想像しているように、これも特権(特権)操作です。ユーザーモードでこの命令を実行しようとすると、ハードウェアは許可しません。
協調スケジューリングシステム#
協調スケジューリングシステムでは、OS はシステムコールや特定の不正な操作が発生するのを待って、CPU の制御を再取得します。
非協調スケジューリングシステム#
非協調スケジューリングシステムでは、OS はクロック割り込み、システムコール、または特定の不正な操作が発生するのを待って、CPU の制御を再取得します。次に実行するプログラムは、オペレーティングシステムのスケジューリングアルゴリズムによって決定されます。
仮想メモリ#
ハードウェアベースの動的再配置#
各 CPU には、ベースレジスタとリミットレジスタの 2 つのレジスタが必要です。プログラムの作成とコンパイル時に、アドレス空間は 0 から始まると仮定されますが、プログラムが実際に実行されると、オペレーティングシステムはその物理メモリ内での実際のロードアドレスを決定し、その開始アドレスをベースレジスタに記録します。プロセスが実行されると、そのプロセスが生成するすべてのメモリ参照は、ハードウェアによって物理アドレス = 仮想アドレス + ベースの計算で処理されます。この方法にはメモリの断片化が存在します(割り当てられたスペースがプログラム内で使用されない)。
セグメンテーション、汎用ベース / リミット#
典型的なアドレス空間には、コード、スタック、ヒープの 3 つの異なる論理セグメントがあります。これらの 3 つの部分を物理メモリに配置するために、各セグメントには一連のベース / リミットレジスタがあり、ハードウェアは仮想アドレスを変換する際にセグメントレジスタを使用します。
たとえば、セグメントレジスタの最初の 2 ビットは仮想アドレスが属するセグメントを識別し、残りの 12 ビットはセグメント内のオフセットを表します。この場合、物理アドレス = オフセット + 対応するセグメントのベースです。リミットレジスタの値はセグメントオーバーフローが発生しないかどうかをチェックするために使用されます。各セグメントにはベース / リミットレジスタの他に、ハードウェアはセグメントの成長方向(スタックは逆方向に成長する可能性があります)も知る必要があります。
セグメンテーションによって引き起こされる問題は、物理メモリがすぐに多くの空きスペースの小さな穴で埋まってしまい、新しいセグメントに割り当てるのが困難になることです。これは ** 外部断片化(external fragmentation)** と呼ばれる問題です。
ページング#
ページングは、プロセスのアドレス空間を固定サイズの単位で分割し、各単位をページと呼ばれるものにします。それに応じて、物理メモリもページフレームに分割され、各ページフレームには仮想メモリページが含まれます。
アドレス空間の各仮想ページが物理メモリ内のどこにあるかを記録するために、オペレーティングシステムは通常、ページテーブルと呼ばれるデータ構造を各プロセスごとに保持します。
ページテーブルの主な目的は、アドレス空間の各仮想ページのアドレス変換を保存することで、各ページが物理メモリ内のどこにあるかを知ることができます。ページテーブルは各プロセスごとに 1 つの(プロセスごとの)データ構造です。
この変換プロセスで生成された仮想アドレスを変換するためには、まずそれを 2 つのコンポーネントに分割する必要があります:仮想ページ番号(VPN)とオフセット。プロセスの仮想アドレス空間が 64 バイトであると仮定すると、仮想アドレス全体には 6 ビットが必要です($2^6=64$)。ページサイズが 16 バイトの場合、オフセットには 4 ビットが必要です($2^4=16$)、残りの 2 ビットは VPN に割り当てられます($2^2$ で 4 ページを表すことができ、$4*16=64$)。
アドレス変換を行う際、オペレーティングシステムはページテーブルをクエリし、VPN を PFN(物理ページ番号)に変換します。オフセットは変わらずに物理アドレスを取得した後、データにアクセスするためにさらに 1 回のメモリアクセスが必要です。追加のメモリアクセスのオーバーヘッドは非常に大きく、プロセスのパフォーマンスを 2 倍以上低下させる可能性があります。
したがって、解決する必要がある 2 つの実際の問題があります。ハードウェアとソフトウェアを慎重に設計しないと、ページテーブルはシステムの実行速度を遅くし、あまりにも多くのメモリを使用します。
高速アドレス変換 - TLB#
TLB(Translation Look-aside Buffer)はハードウェアベースのキャッシュであり、アドレス変換の遅さの問題を解決することができます。ハードウェアは仮想アドレスを変換する際にまず TLB がヒットするかどうかを確認し、ヒットしない場合はメモリ内のページテーブルにアクセスします。
TLB に含まれる仮想から物理のアドレスマッピングは現在のプロセスにのみ有効であり、他のプロセスには意味がありません。したがって、プロセスのコンテキストスイッチが発生する場合、ハードウェアまたはオペレーティングシステム(またはその両方)は、次に実行されるプロセスが以前のプロセスのアドレスマッピングを誤って読み取らないように注意する必要があります。
- 単純に TLB をクリアする。
- TLB にアドレス空間識別子(どのプロセスに属するかを示す識別子)を追加する。
マルチレベルページテーブル#
ページテーブルは多くのメモリを使用するため、ページテーブルのサイズを減らすための簡単な方法は、より大きなページを使用することです(VPN の数が減り、オフセットが増えます)。ただし、より大きなページはより多くのメモリ断片化を意味するため、別の方法はセグメンテーションとページングを組み合わせることです。
ページテーブルをハッシュテーブルや木のようなデータ構造で保存することもできますが、二段階ページングと呼ばれる方法を使用することもできます。
ページテーブルを仮想アドレス空間の固定サイズの単位で分割し、それぞれの単位をページと呼びます。それに応じて、物理メモリもページフレームに分割され、各ページフレームには仮想メモリページが含まれます。
アドレス空間の各仮想ページが物理メモリ内のどこにあるかを記録するために、オペレーティングシステムは通常、ページテーブルと呼ばれるデータ構造を各プロセスごとに保持します。
ページテーブルの主な目的は、アドレス空間の各仮想ページのアドレス変換を保存することで、各ページが物理メモリ内のどこにあるかを知ることができます。ページテーブルは各プロセスごとに 1 つの(プロセスごとの)データ構造です。
この変換プロセスで生成された仮想アドレスを変換するためには、まずそれを 2 つのコンポーネントに分割する必要があります:仮想ページ番号(VPN)とオフセット。プロセスの仮想アドレス空間が 64 バイトであると仮定すると、仮想アドレス全体には 6 ビットが必要です($2^6=64$)。ページサイズが 16 バイトの場合、オフセットには 4 ビットが必要です($2^4=16$)、残りの 2 ビットは VPN に割り当てられます($2^2$ で 4 ページを表すことができ、$4*16=64$)。
アドレス変換を行う際、オペレーティングシステムはページテーブルをクエリし、VPN を PFN(物理ページ番号)に変換します。オフセットは変わらずに物理アドレスを取得した後、データにアクセスするためにさらに 1 回のメモリアクセスが必要です。追加のメモリアクセスのオーバーヘッドは非常に大きく、プロセスのパフォーマンスを 2 倍以上低下させる可能性があります。
したがって、解決する必要がある 2 つの実際の問題があります。ハードウェアとソフトウェアを慎重に設計しないと、ページテーブルはシステムの実行速度を遅くし、あまりにも多くのメモリを使用します。