Operating Systems Development Series | |
Virtual Memory
はじめにお帰りなさい!もうチュートリアルの18回目に突入したんですね。ほらね。OSの開発も悪くないでしょ?前回のチュートリアルでは、物理メモリ管理について調べ、完全に動作する物理メモリ・マネージャを開発しました。このチュートリアルでは、ページングと仮想メモリを導入することで、新たなレベルへ挑戦します。このチュートリアルでは、ページングと仮想メモリを紹介し、どのように仮想アドレス空間を再現し、どのように仮想メモリを管理するのかを学びます。 この章のリストはこちらです。
このチュートリアルは、前章で開発した物理メモリマネージャを基に構成されています。 これは、メモリ管理に関する最後の章になるかもしれません! このことを念頭に置いて、さっそく始めましょう。 仮想メモリの概念仮想化の必要性なぜ「仮想メモリ」のことを気にしなければならないのか、不思議に思うかもしれません。結局のところ、私たちはすでにメモリを管理する素晴らしい効果的な方法を持っていますよね?まあ、そんなところです。メモリブロックをうまく管理することはできますが、物理メモリマネージャが行うことはそれだけです。これだけでは、かなり無駄だと思いませんか?仮想メモリとその必要性をより良く理解するために、非常に重要なコンセプトがたくさんあります。 今のところ、物理メモリを直接、間接的に操作する方法しかありませんが、これには大きな問題がたくさんあります。その1つが、存在しないメモリブロックにアクセスする場合です。プログラムとデータの両方がメモリ上に存在するため、プログラムが互いのメモリ空間にアクセスしたり、知らないうちに自分自身や他のプログラムを破壊して上書きしてしまうこともあり得ます。何しろ、メモリの保護がないのですから。 また、ファイルやプログラムをメモリ上の連続した領域にロードできるとは限りません。 このような場合、フラグメンテーションが発生します。例えば、2つのプログラムを読み込んだとします。1つは0x0に、もう1つは0x900にあります。どちらのプログラムもファイルをロードするように要求しているので、データファイルをロードします。 ![]() ここで何が起こっているかに注目してください。これらのプログラムとファイルの間には、たくさんの未使用メモリがあります。さて、もし上記で収まりきらないような大きなファイルを追加したらどうなるでしょうか?このとき、現在の方式では大きな問題が発生します。現在実行中のプログラムや読み込んだファイルを壊してしまうので、特定の方法で直接メモリを操作することはできないのです。 このように、物理メモリを操作する際には多くの問題が発生します。 オペレーティングシステムがシングルタスク(一度に1つのリング0プログラムしか実行しない)であれば、これでよいかもしれません。もっと複雑なものであれば、システム内でメモリがどのように動作するかをもっと制御する必要があります。必要なのは、物理メモリを抽象化し、その詳細を気にする必要がないようにする方法です。そこで、仮想化の出番となるわけです。では、見てみましょう。 仮想メモリコンセプト仮想メモリとは何かを理解するのは、少し難しいかもしれません。仮想メモリは、ハードウェアとソフトウェアの両方によって実装された特別なメモリアドレス指定スキームです。物理的に連続しないメモリを、連続したメモリであるかのように動作させることができます。メモリ・アドレス指定方式」と言ったことに注意してください。これは、仮想メモリによって、メモリアドレスが何を指すかを制御できることを意味します。 仮想アドレス空間(VAS)仮想アドレス空間は、プログラムのアドレス空間です。 物理メモリとは関係ないことに注意する必要がある。各プログラムがそれぞれ独立したアドレス空間を持つことで、あるプログラムが別のプログラムにアクセスしても、別のアドレス空間を使っているため、アクセスできないようにするためのものです。VASは 仮想的なものであり、物理的なメモリとは直接関係ないため、ディスクドライブなどの他のソースをメモリであるかのように使用できます。 つまり、システムに物理的に搭載されている以上の「メモリ」を使用することができます。 これにより、「メモリが足りない」という問題を解決することができます。 また、各プログラムは独自のVASを使用するため、各プログラムは常にベース0x0000:0000で始まるようにすることができます。これにより、先に述べた再配置の問題や、メモリの断片化も解決され、各プログラムに連続した物理的なメモリブロックを割り当てる心配がなくなります。 仮想アドレスは、カーネルがMMUを介してマッピングします。これについては、もう少し後で説明します。 メモリ管理ユニット(MMU)メモリ管理ユニット(MMU)(ページドメモリ管理ユニット(PMMU)とも呼ばれる)は、マイクロプロセッサと メモリコントローラの間に(またはその一部として)設定されます。メモリコントローラの主な機能は、メモリアドレスを物理メモリロケーションに変換することですが、MMUの目的は、仮想メモリアドレスをメモリコントローラで使用するためのメモリアドレスに変換することです。つまり、ページングを有効にすると、すべてのメモリ参照はまずMMUを経由することになるのです。 翻訳ルックサイドバッファ(TLB)これは、仮想アドレス変換の速度を向上させるために使用されるプロセッサ内に格納されるキャッシュです。通常はCAM(Content-Addressable Memory)の一種で、検索キーが変換する仮想アドレス、結果が物理フレームアドレスとなる。アドレスがTLBにない場合(TLBミス)、MMUはページテーブルを検索してアドレスを見つけます。TLBで見つかった場合は、TLBヒットとなります。TLBミス時にページが見つからなかったり、ページテーブルの内部が無効だったりすると、プロセッサはページフォルト例外を発生させます。TLBは、RAMではなくキャッシュに格納されるページのテーブルだと考えてください。 これは重要なことです。ページはページテーブルに格納されます。このページテーブルは、物理アドレスが仮想アドレスにどのように変換されるかを記述するために設定されています。言い換えればTLBは、私たちが設定したページテーブルを使って、仮想アドレスを物理アドレスに変換するのです。そう、その通りです。私たちは、どの仮想アドレスが何にマッピングされるかを設定します。この方法は後で少し見てみましょう。心配しないでください、そんなに悪いことではありませんよ。) ページングされた仮想メモリ仮想メモリは、システム内に実際に存在する以上のメモリを間接的に使用する方法も提供します。一般的な方法としては、ハードディスクやスワップパーティションに保存されたページファイルを使用する方法があります。仮想メモリはハードウェアレベルで処理されるため、動作させるにはハードウェアデバイスコントローラを介してマッピングされる必要があります。これは通常MMUを通して行われますが、MMUについては後で説明します。 仮想メモリの使用例を見るために、実際に動作しているところを見てみましょう。 ![]() ここで何が起こっているかに注目してください。仮想アドレス内の各メモリブロックはリニアです。各メモリブロックは、実際の物理RAM内の位置か、ハードディスクなどの別のデバイスにマッピングされます。ブロックは、これらのデバイス間で必要に応じてスワップされます。これは遅く見えるかもしれませんが、MMUのおかげで非常に高速です。 覚えておいてください。各プログラムは、上に示したような独自の仮想アドレス空間を持っています。各アドレス空間はリニアで、0x0000:00000から始まるので、メモリの断片化やプログラムの再配置に関する問題の多くを解決することができます。 また、仮想メモリはメモリブロックを使用する際に異なるデバイスを使用するため、システム内のメモリ量以上を容易に管理することができます。もしメモリが足りなくなったら、このページファイルを必要に応じて増やすか、警告やエラーメッセージを表示することができます。 各メモリ「ブロック」は「ページ」と呼ばれ、通常4096バイトの大きさになっています。ページについては後で少し説明します。 さて、ページとはメモリブロックのことです。このメモリブロックは、メモリ内のある場所にマッピングされるか、ハードディスクのような他のデバイスの場所にマッピングされます。これはマッピングされていないページです。もしソフトウェアがマッピングされていないページにアクセスした場合(そのページは現在メモリ上にない)、何らかの方法で読み込む必要があります。これはページフォルトハンドラによって行われます。 すべて後で説明しますので、難しく感じても心配しないでください :) 私たちは一般的なページングについて話しているので、ページングで使用されるかもしれないいくつかの拡張機能を見てみるのは良い考えだと思います。それでは見てみましょう! PAEとPSE物理アドレス拡張(PAE)PAEはx86マイクロプロセッサの機能で、32ビットシステムが最大64GBの物理メモリにアクセスできるようにします。 PAEをサポートするマザーボードでは、これを実現するために36ラインのアドレスバスを使用します。PAEを有効にしたページングサポート(cr4レジスタのビット5)は、これまで見てきたものとは少し異なっています。しかし、このチュートリアルがさらに複雑になるのを防ぐため、今はまだ見ません。しかし、もし興味があれば、読者の皆さんも調べてみてください ;)ページサイズ拡張(PSE)PSE は x86 マイクロプロセッサの機能で、4KB 以上のサイズのページを許可します。これにより、x86アーキテクチャは、4KBのページとともに、4MBのページサイズ(「巨大ページ」または「ラージページ」とも呼ばれる)をサポートすることができるようになったのです。ページングの世界さあ、狂気の沙汰の始まりです!)はじめにページングの素晴らしく、ひねくれた世界へようこそ!すでに説明した基本的な概念により、ページングと仮想メモリが何であるかがよく理解できたと思います。これは素晴らしいスタートだと思いませんか?OK、クール...でも、実際にどうやって実装するんだろう?x86 アーキテクチャでページングはどのように動作するのでしょうか?ちょっと見てみましょう。 ページページ(メモリページまたは仮想ページとも呼ばれる)は、固定長のメモリブロックです。 このメモリブロックは物理メモリに常駐させることができます。このように考えてください。ページはメモリブロックと、それがどこにあるかを記述します。ページのマッピングとページングの実装方法については、後ほど説明します。i86アーキテクチャでは、このために特定の形式を使用しています。これにより、1つのページと、それが現在どの位置にあるのかを追跡することができます。では、見てみましょう。 ページテーブルエントリ(PTE)ページテーブルのエントリーは、1つのページを表します。ページテーブルについては少し後で説明しますので、あまり気にしないでください。しかし、ページテーブルのエントリがどのようなものかを見ておく必要があります。x86 アーキテクチャでは、ページを扱うための特定のビットフォーマットが定義されているので、それを見てみましょう。
ここで最も重要なのは、おそらくフレームアドレスである。フレームアドレスは、ページが管理する4KBの物理メモリ位置を表しています。これはページングを理解する上で非常に重要なことですが、なぜそうなのかを今すぐ説明するのは難しいです。とりあえず、1つ1つのページがメモリブロックを管理していることだけは覚えておいてください。ページが存在すれば、それは物理メモリ内の4KBの物理アドレス空間を管理します。 ダーティフラグとアクセスフラグは、ソフトウェアではなく、プロセッサが設定します。プロセッサがどのビットを設定するか、つまりメモリ上のどこに位置するかをどうやって知るのか、不思議に思うかもしれません。これについては、後で少し説明します。ただ、これによってソフトウェアやエグゼクティブは、あるページがアクセスされたかどうかをテストすることができる、ということだけは覚えておいてください。 現在フラグは重要なものです。この1ビットで、あるページが現在物理メモリ内にあるかどうかを判断します。物理メモリにある場合は、フレームアドレスが32ビットのリニアアドレスになります。物理メモリにない場合、そのページはハードディスクなどの別の場所に存在する必要があります。 現在フラグが設定されていない場合、プロセッサは構造体の残りのビットを無視します。このため、残りのビットをどのような目的にも使用することができます。これによって、ページフォルトハンドラが呼ばれたときに、ディスク上のページの位置を確認し、必要なときにページをメモリにスワップすることができるようになります。 簡単な例を挙げましょう。このページが、物理的な位置1MB(0x100000)から始まる4KBのアドレス空間を管理することを望むとします。これはつまり、このページが1MBのアドレスに「マッピング」されていることを意味します。 このページを作るには、ページの12〜31ビット(フレームアドレス)に0x100000をセットし、presentビットをセットするだけでよい。ほら、このページは1MBにマップされています。 0x100000は4KBアラインであることに注意してください。これは3(最初の2ビットを設定する11バイナリ)とORしています。上の表から、このページが物理メモリに存在することを意味する、存在フラグと読み書きフラグが設定されていることがわかります。これは、物理アドレス0x100000からマッピングされているためです)、書き込み可能になっています。 これで終わりです。この例は次の数節でさらに拡張され、すべてがどのように組み合わされるかがわかるようになります。 また、PTEは何も特別なものではありません。PTEについて特別なのは、それをどのように使うかです。これについては、後で少し見てみましょう。 pte.hとpte.cpp - ページテーブルエントリとページの抽象化このデモでは、ページテーブルエントリの個々のプロパティを設定したり取得したりするコードはすべて、この2つのファイルに隠されています。これらはすべて、上記のリストで見てきた32ビットパターンから、ビットとフレームアドレスを設定・取得するものです。このインターフェースは若干のオーバーヘッドがありますが、可読性を大幅に向上させ、作業を容易にします。まず最初に行うのは、ページテーブルエントリで使用されるビットパターンを抽象化することです。これは簡単すぎる。 上のリストで見たビットフォーマットとどのように一致しているかに注目してください。 私たちが欲しいのは、これらのプロパティ(つまりビット)の設定と取得をインターフェイスの後ろに抽象化する方法です。 これを行うには、まず、ページテーブルのエントリを格納するために使用されるデータ型を抽象化します。この例では、単純な uint32_t です。 簡単なことです。次に、これらのビットを設定したり取得したりするために使用されるインターフェイス・ルーチンです。その代わりに、インターフェイスに焦点を当てたいと思います。 pt_entry_add_attrib()は pt_entry 内の 1 つのビットを設定します。pt_entry_add_attrib() は pt_entry 内の単一のビットを設定します。設定するためにマスク (I86_PTE_PRESENT ビットマスクのように) を渡します。pt_entry_del_attrib()も同じですがビットはクリアされます。 pt_entry_set_frame()はフレームアドレス (I86_PTE_FRAME mask) をマスクして、フレームアドレスを設定します。 これらのルーチンには特別なものはありません。ビットマスクやビットフィールドを使用すれば、これらの属性を簡単に手動で設定したり取得したりすることができます。私は個人的には、このセットアップの方がずっと作業がしやすいと感じています;) さて、このセットアップによって、1つのページを追跡できるようになるのは素晴らしいことです。しかし、典型的なシステムでは多くのページを持つ必要があるため、これだけでは意味がありません。そこで、ページテーブルの出番です。 ページテーブルページテーブル...うーん...この言葉、どこかで聞いたことがあるような?*1行上を見てください*。ああ、そうだった;)ページ・テーブルとは、そう、ページの表です。(ページテーブルは、ページが物理アドレスと仮想アドレスの間にどのようにマッピングされるかを追跡することができます。このテーブルの各ページエントリは、前のセクションで示した形式に従っています。つまり、ページテーブルはページテーブルエントリ(PTE)の配列です。 非常にシンプルな構造ですが、非常に重要な目的を持っています。ページテーブルは、それが含むすべてのページのリストと、それらがどのようにマッピングされるかを含んでいます。マッピング」とは、仮想アドレスが物理フレームアドレスにどのように「マッピング」されるかを意味します。ページテーブルはまた、ページ、それらが存在する天気、それらがどのように格納されているか、あるいはそれらがどのプロセスに属しているか(これはページのAVAILビットを使用することによって設定することができる)を管理する。これはシステムの実装に依存し、必要ないかもしれません)。 ちょっと立ち止まってみましょう。1ページが4KBの物理アドレス空間を管理することを思い出してください。それ自体は、物理メモリの特定の4KBの領域のプロパティを記述する32ビットのデータ構造以外の何ものでもありません(前にこれを覚えていますか?)各ページは物理メモリの4KBを「管理」するので、1024ページをまとめると、1024*4KB=4MBの管理仮想メモリとなります。では、どのようにセットアップされているのか見てみましょう。 ![]() これがページテーブルの例です。これはページテーブルの例で、1024ページのエントリーが配列されているだけです。各ページが4KBの物理メモリを管理することを知っていれば、この小さなテーブルをそれ自身の仮想アドレス空間に変えることができます。どうすればいいのでしょう?簡単です。仮想アドレスの形式を決めればいいのです。 例えば、次のような例があります。例えば、次のような仮想アドレスの形式を設計したとしましょう。 これは、仮想アドレスのフォーマットです。つまり、ページングを有効にすると、すべてのメモリアドレスは上記の形式に従うようになります。例えば、次のような命令があったとします。
ここで、0xc0000は 仮想アドレスのように扱われます。分解してみましょう。
今やっていることは、アドレス変換の一例です。この仮想アドレスがどの物理的な場所を指しているかを確認するために、実際に変換しているのです。ページ・テーブル・インデックス、11000000b = 192。これは、ページ・テーブルのページ・エントリです。これで、このページが管理する4KBのベース物理アドレスがわかります。このページが存在する場合(ページ存在フラグが設定されている)、メモリにアクセスするために必要なのはページのフレームアドレスにアクセスするだけです。このページが存在しない場合、ページフォルトを発生させます--ページデータはディスクのどこかにあるかもしれません。ページフォルトハンドラによって、ページの 4KB データをどこかのメモリにコピーし、ページをpresentに設定し、物理メモリのこの新しい 4KB ブロックを指すようにそのフレームアドレスを更新することができるのです。 OK OK、わかっています。この「仮想アドレス」を作成する小さな例は馬鹿げていると思われるかもしれませんが、どうでしょう? これが実際のやり方なのです。実際の仮想アドレスの形式はもう少し複雑で、2つのセクションの代わりに3つのセクションがあります。 ここまでで、すべてがどのように組み合わされるのか、そしてページテーブルの重要性がわかってきたと思います。 ページサイズページサイズが小さいシステムは、ページサイズが大きいシステムよりも多くのページを必要とします。テーブルがすべてのページを追跡するため、ページサイズが小さいシステムでは、追跡するページが多くなるため、より大きなページテーブルが必要になります。簡単でしょう?i86アーキテクチャは4MB(ページアドレス拡張(PAE)を使用する場合は2MBページ)および4KBサイズのページをサポートしています。 注意すべき点は以下の通りです。ページサイズがページテーブルのサイズにどのように影響するかに注目する。 ページディレクトリテーブル(PDT)よし...これでほぼ完成です。ページテーブルは非常に強力な構造体であることがおわかりいただけたと思います。前回の仮想アドレスの例を覚えていますか?各仮想アドレスが2つの部分で構成されている仮想アドレスシステムの例を挙げました。ページテーブルエントリと、そのページへのオフセットです。x86アーキテクチャでは、仮想アドレス形式は2つのセクションではなく、3つのセクションを使用します。ページディレクトリテーブルのエントリ番号、ページテーブルインデックス、そのページへのオフセットです。 ページディレクトリ・テーブルは、ページディレクトリ・エントリの配列にほかなりません。そうなんです、そうなんです...。最後の文章は役に立たないし、情報でもないでしょう? とにかく、まずページディレクトリエントリを見てみましょう。次に、ディレクトリテーブルとその構成について見ていきましょう。 ページ・ディレクトリ・エントリ (PDEs)ページディレクトリエントリは、1つのページテーブルを管理する方法を提供するのに役立ちます。ページテーブルエントリーは、ページテーブルのアドレスを含むだけでなく、それらを管理するために使用できるプロパティを提供します。次のセクションで、このすべてがどのように組み合わされるかを見ることになりますから、まだ理解していなくても心配しないでください。ページ・ディレクトリ・テーブルは、ページ・テーブルと非常によく似た構造になっています。1024個のエントリーの配列で、エントリーは特定のビットフォーマットに従っています。ページディレクトリエントリ(PDE)の形式の良いところは、ページテーブルエントリ(PTE)とほとんど同じ形式に従っていることです(実際、これらは交換可能です)。ほんの少し、細かい部分があるだけです (シャレです ;) )。 以下は、ページディレクトリエントリのフォーマットです。
ここでのメンバーの多くは、以前見たページテーブルエントリ(PTE)のリストで見覚えがあるはずです。 現在値、リード/ライト、アクセスフラグはPTEと同じですが、これらはページではなく、ページテーブルに適用されます。 ページサイズは、ページテーブルの中のページが4KBか 4MBかを決定します。 ページテーブルベースアドレスビットは、ページテーブルの4Kアラインドアドレスを含んでいます。 pde.h と pde.cpp - ページディレクトリエントリの抽象化PTEでやったのと同じように、PDEを抽象化するためのインターフェイスを作りました。難しいことではありません。新しいタイプpd_entryを使って、ページディレクトリエントリを表現しています。また、PTEインターフェースで、ページディレクトリエントリ内のビットを設定したり取得したりするのに使用されるルーチンの小さなセットを提供します。
ページディレクトリ・テーブルを理解するページディレクトリテーブルは1024個のページテーブルの配列のようなものです。各ページテーブルが4MBの仮想アドレス空間を管理することを覚えていますか?では...1024個のページテーブルを組み合わせれば、4GBの仮想アドレスを管理することができるのです。すごいでしょう?さて、少し複雑ですが、それほどでもありません。ページ・ディレクトリ・テーブルは、実際には1024個のページ・ディレクトリ・エントリーの配列であり、上記のフォーマットに従っています。エントリーの形式を振り返って、ページテーブルベースアドレスビットに注目してください。これは、このディレクトリエントリが管理するページテーブルのアドレスです。 視覚的に見た方がわかりやすいかもしれませんので、ご紹介します。 ![]() ここで何が起こっているかに注目してください。各ページのディレクトリエントリは、ページテーブルを指しています。各ページは 4KB の物理メモリ (つまり仮想メモリ) を管理することを覚えていますか?また、ページテーブルは1024ページの配列に過ぎないことを思い出してください。1024*4kb = 4mbです。つまり、それぞれのページテーブルが4MBのアドレス空間を管理していることになります。 各ページディレクトリのエントリーは、各ページテーブルをより簡単に管理する方法を提供してくれます。ページディレクトリ・テーブルは1024個のディレクトリ・エントリの配列であり、各エントリはそれ自身のテーブルを管理するため、実質的に1024個のページ・テーブルを持つことになります。先ほどの計算で、各ページテーブルは4MBのアドレス空間を管理することが分かっています。つまり、1024個のページテーブル*4MBのサイズ=4GBの仮想アドレス空間ということになります。 これで......信じられないかもしれませんが......全部です。ほら、そんなに難しくなさそうでしょう?次の章では、x86の仮想アドレスの本当の形式を再確認し、すべてがどのように連動しているかを理解します。 マルチタスクでの使用しかし、ここでちょっとした問題が発生します。1つのページディレクトリ・テーブルが4GBのアドレス空間を表していることを思い出してください。一度に1つのページディレクトリしか持てないなら、どうやって複数のプログラムに4GBのアドレス空間を持たせるのでしょうか?できない。とにかく、ネイティブではありません。多くのマルチタスクOSは、上位2GBを「カーネル空間」、下位2GBを「ユーザー空間」として、自分用のアドレス空間をマッピングしています。ユーザースペースはカーネルスペースに触れることができません。カーネルのアドレス空間が各プロセスの4GBの仮想アドレス空間にマッピングされているため、現在どのプロセスが実行されていても、カーネルを使用してエラーなく現在のページディレクトリを切り替えることができます。これは、カーネルが常にプロセスのアドレス空間の同じ場所に位置しているために可能なことです。また、これによってスケジューリングも可能になります。詳しくは後述しますが... 仮想メモリ管理ここまでで、優れた仮想メモリマネージャを開発するために必要なことはすべて網羅しました。仮想メモリマネージャはページ、ページテーブル、ページディレクト リテーブルを割り当て、管理する方法を提供しなければなりません。これらのそれぞれを別々に見てきましたが、それらがどのように一緒に働くかについては見てきませんでした。ハイヤーハーフカーネル概要ハイヤーハーフカーネルは、2GB以上の仮想ベースアドレスを持つカーネルです。多くのオペレーティングシステムがハイヤーハーフカーネルを備えています。Windowsカーネルは2GBまたは3GBの仮想アドレスにマッピングされ(/3gbカーネルスイッチが使用されているかどうかに依存)、Linuxカーネルは3GBの仮想アドレスにマッピングされます。 このシリーズは3GBにマッピングされたハイハーイフカーネルを使用します。 ハイハーイフカーネルは仮想アドレス空間に適切にマッピングされなければなりません。これを実現する方法はいくつかありますが、そのいくつかをここで紹介します。 なぜハイハーフカーネルが必要なのか、興味を持たれるかもしれません。カーネルをもっと低い仮想アドレスで動作させることは十分に可能です。その理由のひとつは v86 タスクに関係しています。v86 タスクをサポートする場合、v86 タスクはユーザモードで、リアルモードのアドレス制限 (0xffff:0xffff) 内、つまり約 1MB+64k のリニアアドレス内でしか実行することができません。また、ソフトウェアは通常、高いメモリ位置にアクセスする必要がないため、最初の2GB(一部のOSでは3GB)でユーザーモードのプログラムを実行するのが一般的です。 方法1最初の設計は、ブートローダに一時ページディレクトリを設定させるというものです。 これを使えば、カーネルのベースアドレスは3GBにすることができます。ブートローダはこのベースアドレスに物理アドレス(通常1MB)をマッピングし、カーネルのエントリポイントを呼び出します。 この方法は有効ですが、カーネルが仮想メモリを管理する際にどのように動作させるかという問題が発生します。カーネルは、ブートローダが設定したページディレクトリとテーブルで動作させようとするか、新しいページディレクトリを作って管理するか、どちらかです。新しいページディレクトリを作成する場合、カーネルは自分自身を再マッピング(1MBの物理アドレスとカーネルのベース仮想アドレス)するか、既存の一時ページディレクトリを新しいページディレクトリにクローンする必要があります。 現時点では、この方法がシリーズで採用されています。シリーズのブートローダは、一時的なページディレクトリを設定し、カーネルを3GBの仮想にマッピングします。その後、カーネルはVMM初期化中に新しいページディレクトリを作成し、自分自身を再マップします。カーネルはこのセットアップ段階でも位置非依存でなければなりません。 この方法は、当社の社内OSで使用している方法です。 方法2もう1つの可能な設計は、ブートローダがカーネルを物理メモリロケーションにロードし、ページングを無効にしたままにすることです。カーネルの仮想ベースアドレスは、そのカーネルが実行すべき仮想アドレスになります。たとえば、ブートローダはカーネルのベースアドレスが3GBであるにもかかわらず、物理的に1MBの場所にカーネルをロードして実行することができるのです。 この方法は少し厄介です。ブートローダがカーネルをどの物理アドレスでロードし実行するかを知る方法がなければならず、カーネルはそれ自体を実際のベース仮想アドレスにマップしなければならないのです。これは通常、位置非依存なコードでカーネル起動時に行われます。位置依存のコードでも使えるが、カーネルはデータへのアクセスや関数の呼び出しの際にアドレスを固定できるようにしなければならない。弊社内製OSではこの方式を採用しています。 方法3Tim Robinson氏のGDTトリックを利用した方法です。この方法を使うと、カーネルがロードされていないにもかかわらず、より高いアドレス(ベースアドレス)で動作させることができます。このトリックは、アドレスの回り込みによって機能します。例えば、カーネルが1MBの物理アドレスにロードされているが、3GBの仮想アドレスで動作しているように見せかけるとします。この場合、X + 3GB = 1MB が基本です。もっと詳しく見てみましょう。 GDTディスクリプタのベースアドレスはDWORDであることを思い出してください。この値が0xffffffより大きくなると、ラップアラウンドして0に戻ります。0xffffff - 0xc0000000 = 0x3FFFFFFF バイトがラップするまで残っています。このアドレスが物理的な位置(1MB)を指すようにするために、アドレスを追加する必要があります。DWORDがラップして0に戻るまで0x3FFFFFFバイト残っているので、0x100000(1MB)+0x3FFFFFF=0x400FFFFF+1=0x40100000を追加すればいいのです。 つまり、上記の例では、カーネルが1MBの物理アドレスでロードされているが、実際のベースアドレスは3GBの仮想アドレスである場合、ベースコードとデータセレクタが0x40100000であるテンポラリGDTを作成することができるのです。プロセッサは、アクセスしているアドレスにベースセレクタのアドレスを自動的に追加します。LGDTを使用して、この新しいGDTをインストールした後。この後、我々は3GBで動作するようになりました。これは、プロセッサがcsとdsセレクタのベース(40100000)を、参照されているアドレスに追加するからです。 たとえば、3GBは、この例では3GB+ベースセレクタ((40100000)=1MB物理)とプロセッサによって1MBに変換されるのです。 このトリックはかなり簡単に実装でき、うまく機能しますが、64ビット(ロングモード)には使えません。カーネルはこのトリックを実行した後、ページディレクトリをセットアップし、ページングを有効にすることができますし、簡単に自分自身をマップすることができます。 仮想アドレスとマッピングアドレスページングを有効にすると、すべてのメモリ参照は 仮想アドレスとして扱われます。これは非常に重要なことです。つまり、ページングを有効にする前に、まず構造体を適切にセットアップしなければならないのです。そうしないと、有効な例外ハンドラの有無にかかわらず、没個性的なトリプルフォールトに遭遇する可能性があります。仮想アドレスの形式を覚えていますか?これはx86の仮想アドレスの形式です。 これは非常に重要なことです。これは、プロセッサ(そして私たち)にとって、とても重要な情報です。 ディレクトリ・インデックスの部分は、現在のページ・ディレクトリのどのインデックスを見ればよいかを示しています。前のセクションのディレクトリエントリ構造形式まで振り返ってみてください。各ディレクトリテーブルエントリは、ページテーブルへのポインタを含んでいることに注意してください。また、そのセクションの画像内でも見ることができます。 ディレクトリテーブル内の各インデックスはページテーブルを指しているので、どのページテーブルにアクセスしているかが分かります。 ページテーブルインデックスの部分は、このページテーブルの中のどのページエントリにアクセスしているのかを教えてくれます。 ...そして、各ページエントリは4KBの物理アドレス空間を管理することを思い出してください。ページへのオフセットは、このページの物理アドレス空間内のどのバイトを参照しているかを教えてくれます。 ここで何が起こったかに注目してください。ページテーブルを使って仮想アドレスを物理アドレスに変換しているのです。 そう、とても簡単なのです。何のトリックもありません。 別の例を見てみましょう。仮想アドレス0xC0000000が物理アドレス0x100000にマップされたと仮定します。どうすればいいのでしょう?0xC0000000が参照しているページを構造体の中から探す必要があります。この場合、0xC0000000は仮想アドレスなので、その形式を見てみましょう。
ディレクトリインデックスが、ページディレクトリテーブルの中でどのページテーブルにアクセスしているのかを教えてくれることを覚えていますか?つまり... 1100000000b (ディレクトリインデックス) = 768番目のページテーブルです。 ページテーブルインデックスは、このページテーブルの中でアクセスしているページであることを思い出してください。これは0なので、最初のページということになります。また、このページのオフセットバイトは0であることに注意してください。 あとは、768 番目のページ・テーブルの最初のページのフレーム・アドレスを 0x100000 に設定すれば、できあがりです。これで、3GBの仮想アドレスが1MBの物理アドレスにマッピングされたことになります!各ページは4KBアラインメントなので、これを4KBの物理アドレス単位で続けていけばよいのです。 IDマッピングIDマッピングは、仮想アドレスを同じ物理アドレスにマッピングすることに他なりません。例えば、仮想アドレス0x100000は物理アドレス0x100000にマッピングされます。そう、それがすべてなのです。実際にこの作業が必要なのは、ページングを最初に設定するときだけです。これは、ページングを有効にしたときに、現在実行中のコードのメモリアドレスが同じままであることを保証するのに役立ちます。これを行わないと、すぐにトリプルフォールトが発生します。この例は、Virtual Memory Managerの初期化ルーチンで見ることができます。メモリ管理。インプリメンテーションインプリメンテーションこれですべてだと思います。次に見るのは、このチュートリアルのために開発された仮想メモリマネージャ(VMM)そのものです。このチュートリアルのために開発された仮想メモリマネージャ(VMM)そのものを紹介します。ルーチンを小さくして、一度に1つのトピックに集中できるようにしました。 さて、まずページテーブルとディレクトリテーブルを見てみましょう。 physical_addrタイプと同様に、仮想メモリ用に新しいアドレスタイプ、virtual_addr を作成しました。 ページテーブルは 1024 個のページテーブルエントリの配列に過ぎないことに注意してください?ページ・ディレクトリ・テーブルも同じですが、代わりにページ・ディレクトリ・エントリの配列になっています。まだ特別なことは何もありません ;) PAGE_DIRECTORY_INDEX, PAGE_TABLE_INDEX, PAGE_GET_PHYSICAL_ADDRESSは仮想アドレスの各部分を返すだけのマクロである。仮想アドレスは特定のフォーマットを持っていることを覚えておいてください。これらのマクロは、仮想アドレスから情報を取得することを可能にします。 PTABLE_ADDR_SPACE_SIZEは、ページテーブルが表すサイズ(バイト数)を表します。ページテーブルは1024ページで、1ページの大きさは4Kなので、1024 * 4k = 4MBとなります。DTABLE_ADDR_SPACE_SIZEは、ページディレクトリが管理するバイト数を表し、これは仮想アドレス空間のサイズである。ページテーブルがアドレス空間の4MBを占め、ページディレクトリが1024個のページテーブルを含むとすると、4MB * 1024 = 4GBとなります。 ここで紹介する仮想メモリマネージャは、大きなページを扱うことはありません。その代わり、4Kページのみを管理します。 私たちが使っている仮想メモリマネージャ(VMM)は、これらの構造に大きく依存しています。VMM のルーチンのいくつかを見て、それらがどのように動作するかを学びましょう。vmmngr_alloc_page () - 物理メモリにページを割り当てる.ページを割り当てるために必要なことは、そのページが参照する物理メモリの 4K ブロックを割り当て、そこからページテーブルエントリを作成するだけです。PTEルーチンのおかげで、この作業がずっと簡単になったことにお気づきでしょうか。上記は、ページテーブルエントリにPRESENTビットをセットし、そのFRAMEアドレスが割り当てられたメモリブロックを指すようにセットしています。こうしてページは存在し、物理メモリの有効なブロックを指すようになり、使用できるようになりました。クールでしょう? また、物理アドレスをページに「マッピング」していることに注目してください。これは、物理アドレスを指すようにページを設定することを意味します。したがって、ページはそのアドレスに「マップ」されます。 vmmngr_free_page () - 物理メモリ内のページを解放するページを解放するのはもっと簡単です。物理メモリ・マネージャを使ってメモリ・ブロックを解放し、ページ・テーブル・エントリのPRESENTビットをクリア(NOT PRESENTをマーク)するだけです。これで完了です。さて、1つのページを割り当てたり解放したりする方法ができたので、それらをフルページテーブルにまとめることができるかどうか見てみましょう... vmmngr_ptable_lookup_entry () - ページテーブルからアドレス指定でページテーブルエントリを取得する。さて、仮想アドレスからページテーブルのエントリ番号を取得する方法ができたので、ページテーブルからそれを取得する方法が必要です。このルーチンはまさにそれを行います!これは上記の関数を使って仮想アドレスをページテーブル配列のインデックスに変換し、そこからページテーブルエントリを返します。このルーチンはポインタを返すので、必要なだけエントリを変更することができます。どうです? ページテーブルルーチンは以上です。ページングがいかに簡単か、おわかりいただけたでしょうか?) 次は...ページディレクトリのルーチンです。 vmmngr_pdirectory_lookup_entry () - ディレクトリテーブルからアドレスでディレクトリエントリを取得する。さて、仮想アドレスをページディレクトリ・テーブルのインデックスに変換する方法を得たので、そこからページディレクトリ・エントリを取得する方法を提供する必要があります。これはページテーブルルーチンの対応するものと全く同じです。
vmmngr_switch_pdirectory () - 新しいページディレクトリに切り替えます。これらのルーチンのすべてがいかに小さいかに注目してください。これらのルーチンは、ページテーブルとディレクトリを簡単に扱うための最小限の、しかし非常に効果的なインタフェースを提供します。ページディレクトリをセットアップするとき、私たちの使用のためにそれをインストールする方法を提供する必要があります。前のチュートリアルでは、ページ・ディレクトリ・ベース・レジスタ(PDBR)を設定および取得するためにpmmngr_load_PDBR()およびpmmngr_get_PDBR()の 2 つのルーチンを追加しました。これは、現在のページ・ディレクトリ・テーブルを格納するレジスタです。x86アーキテクチャでは、PDBRはcr3プロセッサレジスタである。したがって、これらのルーチンは、単にcr3レジスタを設定および取得します。 vmmngr_switch_pdirectory () は、これらのルーチンを使用して PDBR をロードし、カレントディレクトリを設定します。
vmmngr_flush_tlb_entry () - TLB エントリをフラッシュする。TLB がどのように現在のページテーブルをキャッシュするか覚えていますか?時には、TLB や個々のエントリをフラッシュ(無効化)して、現在の値 に更新できるようにする必要があるかもしれません。 これはプロセッサによって自動的に行われるかもしれません(制御レジスタを含む mov 命令の間など)。プロセッサは、個々のTLBエントリを自分で手動でフラッシュする方法を提供します。 これはINVLPG命令を使用して行われます。 この命令に仮想アドレスを渡すだけで、結果としてページ・エントリが無効になります。 INVLPGは 特権的な命令であることに留意してください。したがって、これを使用するには、スーパーバイザモードで実行する必要があります。 vmmngr_map_page () - マップページこれは最も重要なルーチンの1つです。このルーチンを使うと、任意の物理アドレスを仮想アドレスにマッピングすることができます。少し複雑なので、分解して説明します。
パラメータとして、物理アドレスと仮想アドレスが与えられています。最初にしなければならないのは、この仮想アドレスが位置するページディレクトリ・エントリーが有効かどうか(つまり、以前に割り当てられ、そのPRESENTビットが設定されているかどうか)を確認することです。 ページディレクトリのインデックスは仮想アドレスの一部なので、PAGE_DIRECTORY_INDEX()を使用してページディレクトリのインデックスを取得します。それから、ページディレクトリの配列にインデックスを付けて、ページディレクトリエントリへのポインタを取得するだけです。I86_PTE_PRESENT ビットが設定されているかどうかを確認します。 設定されていない場合、ページディレクトリのエントリが存在しないため、作成する必要があります。
まず、新しいページテーブル用に新しいページを確保し、クリアします。 その後、再びPAGE_DIRECTORY_INDEX()を使用して仮想アドレスからディレクトリインデックスを取得し、ページディレクトリにインデックスを作成してページテーブルエントリへのポインタを取得します。そして、新しいallocateページテーブルを指すようにページテーブルエントリを設定し、それが使用できるようにそのPRESENTビットとWRITABLEビットを設定します。 この時点で、ページテーブルはその仮想アドレスで有効であることが保証されます。ですから、ルーチンは今、アドレスをマップする必要があるだけです...
上記では、ページテーブルエントリを取得するために、PAGE_GET_PHYSICAL_ADDRESS()を呼び出して、ページディレクトリエントリが指す物理フレームを取得しています。次に、PAGE_TABLE_INDEXを使用して仮想アドレスからページテーブルインデックスを取得し、ページテーブルにインデックスを付けて、ページテーブルエントリを取得する。そして、物理アドレスを指すようにページを設定し、ページのPRESENTビットを設定します。 vmmngr_initialize () - VMMを初期化する。これは重要なルーチンです。これは上記のルーチンのすべて(まあ、そのほとんど;)を使って、デフォルトのページディレクトリを設定し、それをインストールし、ページングを有効にします。また、このルーチンは、すべてがどのように機能し、組み合わされるかの例として使用することもできます。このルーチンは新しいページディレクトリを作成するので、カーネルのために、1MBの物理を3GBの仮想にマッピングする必要もあります。これはかなり大きなルーチンなので、分解して何が起こっているのか見てみましょう。 ページテーブルが4Kアラインドアドレスに配置されなければならないことを思い出してください。物理メモリマネージャ(PMM)のおかげで、私たちのpmmngr_alloc_block()はすでにこれを実行しているので、心配する必要はありません。割り当てられた1つのブロックはすでに4Kのサイズなので、ページテーブルはそのエントリにも十分なストレージスペースを持っています(1024ページテーブルエントリ*エントリあたり4バイト(ページテーブルエントリのサイズ)=4K)ので、必要なのは1ブロックのみです。 その後、ページテーブルをクリアして、使用するためにきれいにします。 この部分は少しトリッキーです。ページングが有効になると同時に、すべてのアドレスが仮想的になることを思い出してください。 これが問題になります。これを解決するには、仮想アドレスを同じ物理アドレスにマッピングして、同じものを参照するようにする必要があります。これがアイデニティ・マッピングです。 上記のコードでは、ページテーブルを物理メモリの最初の4MB(ページテーブル全体)にマッピングしています。新しいページを作成し、そのPRESENTビットに続いて、ページが参照したいフレームアドレスを設定します。その後、マッピングしている現在の仮想アドレス(「frame」に格納)をページテーブルインデックスに変換し、そのページテーブルエントリを設定します。 i "に格納されている)ページテーブルの各ページについて、"frame "を4K(4096)ずつインクリメントしています。(ページテーブルインデックス0はアドレス0-4093を参照し、インデックス1はアドレス4096を参照する...と覚えていますか?) ここで問題にぶつかります。ブートローダはカーネルを直接3gbの仮想空間にマッピングしてロードするので、カーネルがある領域も再マッピングする必要があるのです。
このコードは上記のループとほぼ同じで、1MBの物理を3GBの仮想にマッピングしています。これは、カーネルをアドレス空間にマッピングし、カーネルが3GBの仮想アドレスで動作し続けることを可能にするものです。 上記は新しいページディレクトリを作成し、私たちが使用するためにそれをクリアします。 各ページテーブルは4MBの仮想アドレス空間全体を表していることを思い出してください。各ページディレクトリのエントリがページテーブルを指していることを知っていれば、各ページディレクトリのエントリはディレクトリテーブル全体の4GB仮想アドレス空間の中の同じ4MBアドレス空間を表していると安全に言うことができます。ページディレクトリの最初のエントリは最初の4MBを、2番目のエントリは次の4MBを、といった具合です。今は最初の 4MB しかマッピングしていないので、必要なのは最初のエントリをページテーブルを指すように設定することだけです。 同様に、3GBのページディレクトリエントリを設定します。これは、カーネルをマップするために必要です。 また、ページディレクトリエントリPAGEとPRESENTビットも設定していることに注意してください。これは、ページテーブルが存在し、書き込み可能であることをプロセッサに伝えます。 これでページディレクトリが設定されたので、ページディレクトリをインストールし、ページングを有効にします。すべてが期待通りに動けば、プログラムはクラッシュしないはずです。もしうまくいかなければ、おそらくトリプルフォールトになるでしょう。 ページフォルトご存知のように、ページングを有効にするとすぐにすべてのアドレスが仮想化されます。これらの仮想アドレスはすべて、ページテーブルとページディレクトリのデータ構造に大きく依存しています。これはいいのですが、仮想アドレスがまだ有効でないページにアクセスすることをCPUに要求する場合がたくさんあります。このような場合、プロセッサはページ障害例外(#PF)を発生させます。 PFは、ページが存在しないとマークされた場合のみ発生します。 GPFは、ページが適切にマッピングされていないにもかかわらず、存在するとマークされ、アクセス可能である場合に発生します。GPFは、ページがアクセス可能でない場合にも発生します。ページフォルトは、CPU割り込み14で、情報を取得できるようにエラーコードをプッシュします。 プロセッサがプッシュするエラーコードは、次の形式です。
その他のビットは0 PFが発生すると、プロセッサはCR2レジスタに故障の原因となったアドレスも格納します。 通常、#PFが発生すると、OSは現在実行中のプログラムの故障したアドレスからページをディスクからフェッチする必要があります。 これには、OSのいくつかの異なる構成要素(ディスクドライバ、ファイルシステムドライバ、ボリューム/マウントポイント管理)が必要であり、我々はまだ持っていない。このため、ページフォルトの処理については、もう少し進化した OS ができたときに、また取り上げることにします。 まとめこのチュートリアルを終えて、とてもうれしいです。このチュートリアルでは、多くの情報と領域をカバーしました。仮想メモリ、仮想アドレッシングと変換、ページング、メソッド、その他。このチュートリアルで、私たちはまだページングという言葉から抜け出してはいません。このチュートリアルで、私たちはまだページングという言葉から抜け出してはいません!しかし、私たちは、それがどのように動作し、それを使用するためのホットについて、より良い理解を持っていることを知っているので、安全に今夜眠りにつくことができます。ほらね?そんなに悪くないでしょう?) 次のチュートリアルの中では、キーボードドライバの開発という楽しいことに戻ろうと考えています。すでに出力の形があり、入力を取り出すことができるので、簡単なコマンドラインも作ることができるかもしれません;) |