やかんです。
前回記事(こちら)の流れでOoOのWake Upロジックについて扱うのもいいなと思いましたが、仮想記憶の方がテーマとして広い気がしたので先に仮想記憶を復習します。
仮想記憶とは?
仮想記憶とは、主記憶容量を、OSのサポートのもとディスクを用いることで大きく見せる技術です。
あっちょるか?
ディスクというのは一般的な「ストレージ」をイメージしてください。主記憶というのはRAMと称されたりメインメモリと称されたりしますが、要はCPUが直接アクセス可能なメモリのことです。
余談ですが、主記憶はCPUにとってオフチップなため、キャッシュメモリなどオンチップなメモリの存在がありがたいわけです。
仮想記憶の理解はメモリ管理の理解から始まります。
「始まります。」というか、「僕は始まると思っています。」が正しいです。
メモリ管理の理解といっても大層な話ではなく、「まずはページングを最低限心得ましょう」みたいな話です。
早速ですが、仮想記憶の文脈に照らすと、メモリは「ページ」という単位で管理されています。これは具体的に考えるとわかりやすくて、試しにメモリアドレス幅が32bitで、1ページあたりのサイズが4KBの場合を考えます。
このとき、「全てのメモリアドレスはどこかの『ページ』に存在する必要がある」ということを念の為強く意識することをお勧めします。
あと、バイトアドレッシングを考えるから1メモリアドレスあたり1Bのデータね
1ページあたり4KBということは、1ページの中にデータの格納場所が「4 * 2^10(4かける2の10乗)」通り存在するイメージです。
1Kは2^10(2の10乗)
「4 * 2^10(4かける2の10乗)」は12bitですね。言い換えると、12bitあれば1ページ内のどこに該当するデータがあるか特定できるわけです。
12bitはオフセットとして使用されるということです
はい。天下り的で恐縮ですが、この12bitは、32bitのメモリアドレスのうち下位12bitが用いられます。
となると、全てのメモリアドレスがどこかのページに存在するためには、32bitのメモリアドレスから下位12bitを除いた残りの20bit個のページがあれば事足ります。
というわけなので、メモリアドレス幅32bit、1ページあたりのサイズが4KBの時には、ページの数が2^20(2の20乗)あればOKということになります。
後述しますが、1ページあたりのサイズのことを「ページサイズ」と言います
ページの説明が長くなってきましたが、そんなわけなので「あるメモリにアクセスする」ということは、「そのメモリアドレスが属するページになんらかの形で触れつつメモリにアクセスしている」ということになります。
メモリはページを通じて管理されるわけですね。
仮想記憶関連の用語を整理する。
仮想記憶そのものの話に入る前に、事前に用語を整理します。
仮想記憶は概念自体一筋縄じゃいかない印象ですが、その文脈で用いられる用語のややこしさが理解を妨げている気がしています。なので、用語をまずは整理したい、というモチベです。
物理メモリ
主記憶(RAM、メインメモリ)のことです。
物理ページ
ディスクに存在するページのことで、ページが主記憶(物理メモリ)にページインした場合には形式上「同じ物理ページがディスクにも主記憶(物理メモリ)にも存在する」という状態になります。
仮想ページ
各プロセスが扱うページの概念です。「仮想なんとか」系はちょっとイメージしづらいと思います。差し当たり、「プロセスが直接扱う(扱っているつもりになっている)もの」と思って大丈夫かと。
ちょっとだけ仮想記憶の仕組みに踏み込む
ざっくり述べると、仮想記憶は「各プロセスにはディスクに存在し得るページも含めた大きな論理的なメモリ空間を見せつつ、ディスクと物理メモリ間でページのイン・アウトを適宜繰り返すことで物理的な整合性を取る」というような仕組みになっています。
あっちょるか?
まあ、プロセスには実際の物理メモリよりも大きなメモリがあるかのように見せるけど、その裏側では「大きなメモリがあるかのように見せ」たことで不都合が生じないようにあくせく処理が行われている、みたいなイメージです。
あくせく処理を行ってくれる真面目なあの子は、OSのことです。
いわゆるページフォルトについての説明はここでは端折るので、全然論理的な説明にはならなくなってしまうわけですが、
- ページフォルトが起きる
- ディスクからページが物理メモリにインされる
- CPUがページに当該ページにアクセスできるようになる
といった流れになっています。以前mmapについて復習(こちら)した記事にもちょろちょろっと書いてはあるので、メモすぎて恐縮ですが参考になりましたら幸いでございます。
ページテーブルにそれなりに踏み込む
仮想ページはあくまでも「各プロセスが触っているつもりになっているページ」に過ぎないので、その実態を確保するためには仮想ページと物理ページの対応付を行う必要があります。
「プロセスが仮想ページにアクセスしたぞー!」
「まじか、仮想ページはハリボテだから実態を用意してあげないとバグっちゃう」
「この仮想ページは、あの物理ページに対応付られています!」
「よし。あの物理ページは物理メモリに存在しているか?」
「存在してないです!ページフォルトが起きます!」
「うおー、ディスクからページを読み込めー!」
みたいな流れがページングにおいては生じています。
※あくまでイメージです。
この、
「この仮想ページは、あの物理ページに対応付られています!」
「よし。あの物理ページは物理メモリに存在しているか?」
「存在してないです!ページフォルトが起きます!」
あたりが仮想ページと物理ページの対応付を物語っています。で、これまた天下り的で恐縮ですが、この対応付を行っているのがページテーブルになります。
ページテーブルは「テーブル」という名前の通り、複数のエントリをもち、各エントリにはなんらかの形でインデックスが付与されている構造になっています。各エントリに含まれる情報は多岐にわたるようで、「仮想ページに対応する物理ページはどれかの対応付」「当該物理ページが物理メモリ上に存在してるかのフラグ」のほか、読み取りのみ可能なページかなどのフラグもエントリに含まれています。
が、ここで大事なのは「仮想ページに対応する物理ページはどれかの対応付け」と「当該物理ページが物理メモリ上に存在してるかのフラグ」の2つです。
「読み取りのみ可能なページかなどのフラグ」については確かファイルのAPIについて復習した時(こちら)に触れた気がする。。
さて、ここで「仮想記憶の理解はメモリ管理の理解から始まります。」で扱った、ページのオフセットなどの話を思い出します。
ページ内のオフセットが算出できれば、全てのメモリアドレスをページにマッピングするのに必要なページ数が算出できます。これはつまり、「全てのメモリアドレスをページにマッピングするのに必要なページ数」の算出に利用したメモリアドレス(論理メモリアドレス)の上位bitをページテーブルのインデックスとして利用できることに他なりません。
論理メモリアドレスの上位bitをインデックスとしてページテーブルを引くと、該当するエントリを参照することで、対応する物理ページや、そのページが物理メモリ上に存在しているかなどの情報を得ることができます。ページ内オフセットはそのまま利用します。
で、必要に応じてページインしたりページアウト(スワップ)したり、それらの処理に応じてページテーブルを更新したりするわけです。
ページテーブルの1エントリには想像以上に色々な情報が載っている、というのが理解において大事な気がしている。
余談
余談として、TLBの存在について触れておきます。
「メモリアクセスにおいてはページテーブルを参照する」というわけなのですが、ページテーブルの参照にかかる遅延時間がクリティカルなものになっては元も子もないです。そのため、ページテーブルのエントリをキャッシュしておき、そのインデックスが参照されようとしたらキャッシュを返すような仕組みがあるみたいです。
これをTLB(Translation Lookaside Buffer)と言います。
なので、論理メモリアドレスからインデックスを取得し、実際にメモリアクセスが行われる流れは
- TLBに問い合わせる
- TLBにエントリがキャッシュされていたら終了。なければページテーブルに問い合わせ。
- ページテーブルから対応する物理ページを取得。
- 当該エントリから、ページフォルトが生じるかチェック。
- ページフォルトが生じない場合は特に何もなし。ページフォルトが生じる場合制御をOSに移しページインしてもらう。
- 物理メモリにページが存在することが保証されたので、メモリアクセスを行う
みたいな流れになっています。
最後に
今回の記事は、3Aで受講したOSの授業、現在(4S)で受講しているコンピュータアーキテクチャの授業、そしてこちらのgpt-4oくんとのチャットを元に、超個人的な理解に基づいて書いています。
間違いや不正確な点などありましたら、ぜひ教えていただけますと非常に助かります!
よろしくお願い致します。
ということで、こちらの記事は終了とします。最後までお読みいただき、ありがとうございます。