Fix for the advice given by @garasubo

This commit is contained in:
woodyZootopia
2021-10-07 22:41:09 +09:00
parent 4472eed23c
commit f57bded691

View File

@@ -10,7 +10,7 @@ translation_based_on_commit = "27ab4518acbb132e327ed4f4f0508393e9d4d684"
translators = ["woodyZootopia", "garasubo"]
+++
この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付けを作るための関数を実装します。
この記事では私達のカーネルをページングに<ruby>対応<rp> (</rp><rt>マップ</rt><rp>) </rp></ruby>させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい<ruby>対応付け<rp> (</rp><rt>マッピング</rt><rp>) </rp></ruby>を作るための関数を実装します。
<!-- more -->
@@ -32,7 +32,7 @@ translators = ["woodyZootopia", "garasubo"]
[end of previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburuhenoakusesu
この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しい対応関係を作る方法と、それを作るための未使用メモリを見つける方法を学びます。
この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しいマッピングを作る方法と、それを作るための未使用メモリを見つける方法を学びます。
## ページテーブルにアクセスする
@@ -42,88 +42,88 @@ translators = ["woodyZootopia", "garasubo"]
ここで重要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しては変換せずにすみます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。
問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それに対応づけられている何らかの仮想アドレスを通じてのみ可能です。
問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それにマップさせられている何らかの仮想アドレスを通じてのみ可能です。
そのため、ページテーブルのフレームにアクセスするためには、どこかの仮想ページをそれに対応づけなければいけません。このような、任意のページテーブルのフレームにアクセスできるようにしてくれる対応付けを作る方法にはいくつかあります。
そのため、ページテーブルのフレームにアクセスするためには、どこかの仮想ページをそれにマッピングしなければいけません。このような、任意のページテーブルのフレームにアクセスできるようにしてくれるマッピングを作る方法にはいくつかあります。
### 恒等対応
### 恒等マッピング
シンプルな方法として、**すべてのページテーブルを恒等対応させる**ということが考えられるでしょう:
シンプルな方法として、**すべてのページテーブルを恒等<ruby>対応<rp> (</rp><rt>マップ</rt><rp>) </rp></ruby>させる**ということが考えられるでしょう:
![A virtual and a physical address space with various virtual pages mapped to the physical frame with the same address](identity-mapped-page-tables.svg)
この例では、いくつかの恒等対応したページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスと同じ値になり、よってCR3レジスタから始めることで全ての階層のページテーブルに簡単にアクセスできます。
この例では、恒等マップしたいくつかのページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスと同じ値になり、よってCR3レジスタから始めることで全ての階層のページテーブルに簡単にアクセスできます。
しかし、この方法では仮想アドレス空間が散らかってしまい、大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。
しかし、この方法では仮想アドレス空間が散らかってしまい、大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さでマッピングのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。
[memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file
[segmentation]: @/edition-2/posts/08-paging-introduction/index.ja.md#duan-pian-hua-fragmentation
同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリ<ruby>マップト<rp> (</rp><rt>に対応づけられた</rt><rp>) </rp></ruby>ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので使用することができません。
同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリ<ruby>マップト<rp> (</rp><rt>に対応づけられた</rt><rp>) </rp></ruby>ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等マッピングを作ることができないので使用することができません。
### 固定オフセットの対応づけ
### 固定オフセットのマッピング
仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルの対応づけのために別のメモリ領域を使う**ことができます。ページテーブルを恒等対応させる代わりに、仮想アドレス空間で一定の<ruby>補正値<rp> (</rp><rt>オフセット</rt><rp>) </rp></ruby>をおいて対応づけてみましょう。例えば、オフセットを10TiBにしてみましょう
仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルのマッピングのために別のメモリ領域を使う**ことができます。ページテーブルを恒等マップさせる代わりに、仮想アドレス空間で一定の<ruby>補正値<rp> (</rp><rt>オフセット</rt><rp>) </rp></ruby>をおいてマッピングしてみましょう。例えば、オフセットを10TiBにしてみましょう
![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg)
`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルの対応付け専用に使うことで、恒等対応のときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64で用いられている48bit仮想アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。
`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルのマッピング専用に使うことで、恒等マップのときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64で用いられている48bit仮想アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。
この方法では、新しいページテーブルを作るたびに新しい対応付けを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると新しいプロセスを作るときに便利なのですが、これも不可能です。
この方法では、新しいページテーブルを作るたびに新しいマッピングを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると新しいプロセスを作るときに便利なのですが、これも不可能です。
### 物理メモリ全体を対応付け
### 物理メモリ全体をマップす
これらの問題はページテーブルのフレームだけと言わず**物理メモリ全体を対応付け**しまえば解決します:
これらの問題はページテーブルのフレームだけと言わず**物理メモリ全体をマップして**しまえば解決します:
![The same figure as for the offset mapping, but every physical frame has a mapping (at 10TiB + X) instead of only page table frames.](map-complete-physical-memory.svg)
この方法を使えば、私達のカーネルは他のアドレス空間を含め任意の物理メモリにアクセスできます。用意する仮想メモリの範囲は以前と同じであり、違うのは全てのページが対応付けられているということです。
この方法を使えば、私達のカーネルは他のアドレス空間を含め任意の物理メモリにアクセスできます。用意する仮想メモリの範囲は以前と同じであり、違うのは全てのページがマッピングされているということです。
この方法の欠点は、物理メモリへの対応付けを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。
この方法の欠点は、物理メモリへのマッピングを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。
しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]を対応付けに使うことができます。こうすれば、例えば32GiBの物理メモリを対応付けるのにはレベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。
しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]をマッピングに使うことができます。こうすれば、例えば32GiBの物理メモリをマップするのにはレベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。
[huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes
### 一時的な対応関係
### 一時的な<ruby>対応<rp> (</rp><rt>マッピング</rt><rp>) </rp></ruby>
物理メモリの量が非常に限られたデバイスについては、アクセスする必要があるときだけ**ページテーブルのフレームを一時的に対応づける**という方法が考えられます。そのような一時的な対応を作りたいときには、たった一つだけ恒等対応させられたレベル1テーブルがあれば良いです
物理メモリの量が非常に限られたデバイスについては、アクセスする必要があるときだけ**ページテーブルのフレームを一時的にマップする**という方法が考えられます。そのような一時的なマッピングを作りたいときには、たった一つだけ恒等マップさせられたレベル1テーブルがあれば良いです
![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to page with address 0](temporarily-mapped-page-tables.svg)
この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。その8番目のエントリは、アドレス`32KiB`の仮想アドレスページをアドレス`32KiB`の物理アドレスページに対応付けるので、レベル1テーブル自体を恒等対応させています。この図ではその恒等対応`32KiB`のところの横向きの(茶色の)矢印で表しています。
この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。その8番目のエントリは、アドレス`32KiB`の仮想アドレスページをアドレス`32KiB`の物理アドレスページにマップするので、レベル1テーブル自体を恒等マップしています。この図ではその恒等マッピング`32KiB`のところの横向きの(茶色の)矢印で表しています。
恒等対応させたレベル1テーブルに書き込むことによって、カーネルは最大511個の一時的な対応を作ることができます512から、恒等対応に必要な1つを除く。上の例では、カーネルは2つの一時的な対応を作りました:
恒等マップさせたレベル1テーブルに書き込むことによって、カーネルは最大511個の一時的なマッピングを作ることができます512から、恒等マッピングに必要な1つを除く。上の例では、カーネルは2つの一時的なマッピングを作りました:
- レベル1テーブルの0番目のエントリをアドレス`24 KiB`のフレームに対応付けることで、破線の矢印で示されているように`0 KiB`の仮想ページからレベル2ページテーブルの物理フレームへの一時的対応付けを行いました。
- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームに対応付けることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的対応付けを行いました。
- レベル1テーブルの0番目のエントリをアドレス`24 KiB`のフレームにマップすることで、破線の矢印で示されているように`0 KiB`の仮想ページからレベル2ページテーブルの物理フレームへの一時的なマッピングを行いました。
- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームにマップすることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的なマッピングを行いました。
これで、カーネルは`0 KiB`に書き込むことによってレベル2ページテーブルに、`36 KiB`に書き込むことによってレベル4ページテーブルにアクセスできるようになりました。
任意のページテーブルに一時的対応付けを用いてアクセスする手続きは以下のようになるでしょう:
任意のページテーブルに一時的なマッピングを用いてアクセスする手続きは以下のようになるでしょう:
- 恒等対応しているレベル1テーブルのうち、使われていないエントリを探す。
- そのエントリを私達のアクセスしたいページテーブルの物理フレームに対応付ける。
- そのエントリに対応付けられている仮想ページを通じて、対象のフレームにアクセスする。
- エントリを未使用に戻すことで、一時的対応付けを削除する。
- 恒等マッピングしているレベル1テーブルのうち、使われていないエントリを探す。
- そのエントリを私達のアクセスしたいページテーブルの物理フレームにマップする。
- そのエントリにマップされている仮想ページを通じて、対象のフレームにアクセスする。
- エントリを未使用に戻すことで、一時的なマッピングを削除する。
この方法では、同じ512個の仮想ページを対応付けを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しい対応付けを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。
この方法では、同じ512個の仮想ページをマッピングを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しいマッピングを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。
### 再帰的ページテーブル
他に興味深いアプローチとして**再帰的にページテーブルを対応付ける**方法があり、この方法では追加のページテーブルは一切不要です。発想としては、レベル4ページテーブルのエントリのどれかをレベル4ページテーブル自体に対応付けるのです。こうすることにより、仮想アドレス空間の一部を予約しておき、現在及び将来のあらゆるページテーブルフレームをその空間に対応付けているのと同じことになります。
他に興味深いアプローチとして**再帰的にページテーブルをマップする**方法があり、この方法では追加のページテーブルは一切不要です。発想としては、レベル4ページテーブルのエントリのどれかをレベル4ページテーブル自体にマップするのです。こうすることにより、仮想アドレス空間の一部を予約しておき、現在及び将来のあらゆるページテーブルフレームをその空間にマップしているのと同じことになります。
これがうまく行く理由を説明するために、例を見てみましょう:
![An example 4-level page hierarchy with each page table shown in physical memory. Entry 511 of the level 4 page is mapped to frame 4KiB, the frame of the level 4 table itself.](recursive-page-table.png)
[この記事の最初での例][example at the beginning of this post]との唯一の違いは、レベル4テーブルの511番目に、物理フレーム`4 KiB`すなわちレベル4テーブル自体のフレームに対応付けられたエントリが追加されていることです。
[この記事の最初での例][example at the beginning of this post]との唯一の違いは、レベル4テーブルの511番目に、物理フレーム`4 KiB`すなわちレベル4テーブル自体のフレームにマップされたエントリが追加されていることです。
[example at the beginning of this post]: #peziteburuniakusesusuru
CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰関数自らを呼び出す関数に似ているので、**<ruby>再帰的<rp> (</rp><rt>recursive</rt><rp>) </rp></ruby>ページテーブル**と呼ばれます。CPUはレベル4テーブルのすべてのエントリはレベル3テーブルを指していると思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。
実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルを対応付けられた物理フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています
実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルをマップされた物理フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています
![The above example 4-level page hierarchy with 5 arrows: "Step 0" from CR4 to level 4 table, "Step 1" from level 4 table to level 4 table, "Step 2" from level 4 table to level 3 table, "Step 3" from level 3 table to level 2 table, and "Step 4" from level 2 table to level 1 table.](recursive-page-table-access-level-1.png)
@@ -131,9 +131,9 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ
![The same 4-level page hierarchy with the following 4 arrows: "Step 0" from CR4 to level 4 table, "Steps 1&2" from level 4 table to level 4 table, "Step 3" from level 4 table to level 3 table, and "Step 4" from level 3 table to level 2 table.](recursive-page-table-access-level-2.png)
ステップごとにこれを見てみましょうまず、CPUはレベル4テーブルの再帰エントリをたどり、レベル3テーブルに着いたと思い込みます。同じ再帰エントリを再びたどり、レベル2テーブルに着いたと考えます。しかし実際にはまだレベル4テーブルから動いていません。CPUが異なるエントリをたどると、レベル3テーブルに到着するのですが、CPUはレベル1にすでにいるのだと思っています。そのため、次のエントリはレベル2テーブルを指しているのですが、CPUは対応付けられた物理フレームを指していると思うので、私達はレベル2テーブルを読み書きできるというわけです。
ステップごとにこれを見てみましょうまず、CPUはレベル4テーブルの再帰エントリをたどり、レベル3テーブルに着いたと思い込みます。同じ再帰エントリを再びたどり、レベル2テーブルに着いたと考えます。しかし実際にはまだレベル4テーブルから動いていません。CPUが異なるエントリをたどると、レベル3テーブルに到着するのですが、CPUはレベル1にすでにいるのだと思っています。そのため、次のエントリはレベル2テーブルを指しているのですが、CPUはマップされた物理フレームを指していると思うので、私達はレベル2テーブルを読み書きできるというわけです。
レベル3や4のテーブルにアクセスするのも同じやり方でできます。レベル3テーブルにアクセスするためには、再帰エントリを3回たどることでCPUを騙し、すでにレベル1テーブルにいると思い込ませます。そこで別のエントリをたどりレベル3テーブルに着くと、CPUはそれを対応付けられたフレームとして扱います。レベル4テーブル自体にアクセスするには、再帰エントリを4回辿ればCPUはそのレベル4テーブル自体を対応付けられたフレームとして扱ってくれるというわけです(下の青紫の矢印)。
レベル3や4のテーブルにアクセスするのも同じやり方でできます。レベル3テーブルにアクセスするためには、再帰エントリを3回たどることでCPUを騙し、すでにレベル1テーブルにいると思い込ませます。そこで別のエントリをたどりレベル3テーブルに着くと、CPUはそれをマップされたフレームとして扱います。レベル4テーブル自体にアクセスするには、再帰エントリを4回辿ればCPUはそのレベル4テーブル自体をマップされたフレームとして扱ってくれるというわけです(下の青紫の矢印)。
![The same 4-level page hierarchy with the following 3 arrows: "Step 0" from CR4 to level 4 table, "Steps 1,2,3" from level 4 table to level 4 table, and "Step 4" from level 4 table to level 3 table. In blue the alternative "Steps 1,2,3,4" arrow from level 4 table to level 4 table.](recursive-page-table-access-level-3.png)
@@ -146,27 +146,27 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ
<details>
<summary><h4>アドレス計算</h4></summary>
実際の変換の前に再帰的移動を1回または複数回行うことですべての階層のテーブルにアクセスできるということを見てきました。4つのテーブルそれぞれのどのインデクスが使われるかは仮想アドレスから直接計算されていましたから、再帰エントリを使うためには特別な仮想アドレスを作り出す必要があります。ページテーブルのインデクスは仮想アドレスから以下のように計算されていたことを思い出してください
実際の変換の前に再帰的移動を1回または複数回行うことですべての階層のテーブルにアクセスできるということを見てきました。4つのテーブルそれぞれのどのインデクスが使われるかは仮想アドレスから直接計算されていましたから、再帰エントリを使うためには特別な仮想アドレスを作り出す必要があります。ページテーブルのインデクスは仮想アドレスから以下のように計算されていたことを思い出してください:
![Bits 012 are the page offset, bits 1221 the level 1 index, bits 2130 the level 2 index, bits 3039 the level 3 index, and bits 3948 the level 4 index](../paging-introduction/x86_64-table-indices-from-address.svg)
あるページを対応付けているレベル1テーブルにアクセスしたいとします。上で学んだように、このためには再帰エントリを1度辿ってからレベル432のインデクスへと続けていく必要があります。これをするために、それぞれのアドレスブロックを一つ右にずらし、レベル4のインデクスがあったところに再帰エントリのインデクスをセットします
あるページをマップしているレベル1テーブルにアクセスしたいとします。上で学んだように、このためには再帰エントリを1度辿ってからレベル432のインデクスへと続けていく必要があります。これをするために、それぞれのアドレスブロックを一つ右にずらし、レベル4のインデクスがあったところに再帰エントリのインデクスをセットします:
![Bits 012 are the offset into the level 1 table frame, bits 1221 the level 2 index, bits 2130 the level 3 index, bits 3039 the level 4 index, and bits 3948 the index of the recursive entry](table-indices-from-address-recursive-level-1.svg)
そのページのレベル2テーブルにアクセスしたい場合、それぞれのブロックを2つ右にずらし、レベル4と3のインデクスがあったところに再帰エントリのインデクスをセットします
そのページのレベル2テーブルにアクセスしたい場合、それぞれのブロックを2つ右にずらし、レベル4と3のインデクスがあったところに再帰エントリのインデクスをセットします:
![Bits 012 are the offset into the level 2 table frame, bits 1221 the level 3 index, bits 2130 the level 4 index, and bits 3039 and bits 3948 are the index of the recursive entry](table-indices-from-address-recursive-level-2.svg)
レベル3テーブルにアクセスする場合、それぞれのブロックを3つ右にずらし、レベル432のインデクスがあったところに再帰インデクスを使います
レベル3テーブルにアクセスする場合、それぞれのブロックを3つ右にずらし、レベル432のインデクスがあったところに再帰インデクスを使います:
![Bits 012 are the offset into the level 3 table frame, bits 1221 the level 4 index, and bits 2130, bits 3039 and bits 3948 are the index of the recursive entry](table-indices-from-address-recursive-level-3.svg)
最後に、レベル4テーブルにはそれぞれのブロックを4ブロックずらし、オフセットを除いてすべてのアドレスブロックに再帰インデクスを使うことでアクセスできます
最後に、レベル4テーブルにはそれぞれのブロックを4ブロックずらし、オフセットを除いてすべてのアドレスブロックに再帰インデクスを使うことでアクセスできます:
![Bits 012 are the offset into the level l table frame and bits 1221, bits 2130, bits 3039 and bits 3948 are the index of the recursive entry](table-indices-from-address-recursive-level-4.svg)
これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。また、インデクスをページテーブルエントリのサイズ倍、つまり8倍することによって、特定のページテーブルエントリを指すアドレスを計算できます。
これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。また、インデクスをページテーブルエントリのサイズ倍、つまり8倍することによって、特定のページテーブルエントリを指すアドレスを計算できます。
下の表は、それぞれの種類のフレームにアクセスするためのアドレス構造をまとめたものです:
@@ -180,7 +180,7 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ
[octal]: https://en.wikipedia.org/wiki/Octal
ただし、`AAA`がレベル4インデクス、`BBB`がレベル3インデクス、`CCC`がレベル2インデクス、`DDD`対応付けられたフレームのレベル1インデクス、`EEE`がオフセットです。`RRR`が再帰エントリのインデクスです。インデクス3ケタをオフセット4ケタに変換するときは、8倍ページテーブルエントリのサイズ倍しています。
ただし、`AAA`がレベル4インデクス、`BBB`がレベル3インデクス、`CCC`がレベル2インデクス、`DDD`マップされたフレームのレベル1インデクス、`EEE`がオフセットです。`RRR`が再帰エントリのインデクスです。インデクス3ケタをオフセット4ケタに変換するときは、8倍ページテーブルエントリのサイズ倍しています。
`SSSSS`は符号拡張ビットで、すなわち47番目のビットのコピーです。これはx86_64におけるアドレスの特殊な要求の一つです。これは[前回の記事][sign extension]で説明しました。
@@ -196,14 +196,14 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ
// この仮想アドレスに対応するページテーブルにアクセスしたい
let addr: usize = [];
let r = 0o777; // 再帰インデクス
let r = 0o777; // 再帰インデクス
let sign = 0o177777 << 48; // 符号拡張
// 変換したいアドレスのページテーブルインデクスを取得する
let l4_idx = (addr >> 39) & 0o777; // レベル4インデクス
let l3_idx = (addr >> 30) & 0o777; // レベル3インデクス
let l2_idx = (addr >> 21) & 0o777; // レベル2インデクス
let l1_idx = (addr >> 12) & 0o777; // レベル1インデクス
// 変換したいアドレスのページテーブルインデクスを取得する
let l4_idx = (addr >> 39) & 0o777; // レベル4インデクス
let l3_idx = (addr >> 30) & 0o777; // レベル3インデクス
let l2_idx = (addr >> 21) & 0o777; // レベル2インデクス
let l1_idx = (addr >> 12) & 0o777; // レベル1インデクス
let page_offset = addr & 0o7777;
// テーブルアドレスを計算する
@@ -217,9 +217,9 @@ let level_1_table_addr =
sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12);
```
上のコードは、レベル4エントリの最後インデクス`0o777`すなわち511が再帰対応していると仮定しています。この仮定は正しくないので,このコードは動作しません。ブートローダに再帰対応付けを設定させる方法については後述します。
上のコードは、レベル4エントリの最後インデクス`0o777`すなわち511が再帰マッピングしていると仮定しています。この仮定は正しくないので,このコードは動作しません。ブートローダに再帰マッピングを設定させる方法については後述します。
ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスを対応付けられた物理アドレスに変換する方法を示しています。
ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスをマップされた物理アドレスに変換する方法を示しています。
[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html
@@ -248,32 +248,32 @@ let frame = recursive_page_table.translate_page(page);
frame.map(|frame| frame.start_address() + u64::from(addr.page_offset()))
```
繰り返しになりますが、このコード(が正しく実行される)には正しい再帰対応がなされていることが必要となります。この対応付けがあるのなら、空欄になっている`level_4_table_addr`は最初のコード例を使って計算すればよいです。
繰り返しになりますが、このコード(が正しく実行される)には正しい再帰マッピングがなされていることが必要となります。そのようなマッピングがあるのなら、空欄になっている`level_4_table_addr`は最初のコード例を使って計算すればよいです。
</details>
---
再帰的ページングは、ページテーブルのたった一つの対応付けがいかに強力に使えるかを示す興味深いテクニックです。比較的実装するのが簡単であり、ほとんど設定も必要でない(一つ再帰エントリを作るだけ)ので、ページングを使って最初に実装するのに格好の対象でしょう。
再帰的ページングは、ページテーブルのたった一つのマッピングがいかに強力に使えるかを示す興味深いテクニックです。比較的実装するのが簡単であり、ほとんど設定も必要でない(一つ再帰エントリを作るだけ)ので、ページングを使って最初に実装するのに格好の対象でしょう。
しかし、いくつか欠点もあります:
- 大量の仮想メモリ領域512GiBを占有してしまう。私達の使っている48bitアドレス空間は巨大なのでこのことはさしたる問題にはなりませんが、キャッシュの挙動が最適でなくなってしまうかもしれません。
- 現在有効なアドレス空間にしか簡単にはアクセスできない。他のアドレス空間にアクセスするのは再帰エントリを変更することで可能ではあるものの、もとに戻すためには一時的対応付けが必要。これを行う方法については[カーネルをリマップする][_Remap The Kernel_](未訳、また旧版のため情報が古い)という記事を読んでください。
- 現在有効なアドレス空間にしか簡単にはアクセスできない。他のアドレス空間にアクセスするのは再帰エントリを変更することで可能ではあるものの、もとに戻すためには一時的なマッピングが必要。これを行う方法については[カーネルをリマップする][_Remap The Kernel_](未訳、また旧版のため情報が古い)という記事を読んでください。
- x86のページテーブルの方式に強く依存しており、他のアーキテクチャでは動作しないかもしれない。
[_Remap The Kernel_]: https://os.phil-opp.com/remap-the-kernel/#overview
## ブートローダによる補助
これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへの対応付けを作ったり、レベル4テーブルのエントリを再帰的に対応付けたりなどです。問題は、これらの必要な対応付けを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。
これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへのマッピングを作ったり、レベル4テーブルのエントリを再帰的にマッピングしたりなどです。問題は、これらの必要なマッピングを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。
つまり、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんな対応付けも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。
つまり、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんなマッピングも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。
[cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section
- `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかに対応付けます。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体を対応付け](#wu-li-memoriquan-ti-wodui-ying-fu-keru)ことができます。
- `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的に対応付けます。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。
- `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかにマッピングします。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体をマップす](#wu-li-memoriquan-ti-wodui-ying-fu-keru)ことができます。
- `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的にマッピングします。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。
私達のカーネルには、シンプルでプラットフォーム非依存かつページテーブルのフレームでないメモリにもアクセスできるのでより強力である1つ目の方法を採ることにします。必要なブートローダの<ruby>機能<rp> (</rp><rt>feature</rt><rp>) </rp></ruby>を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。
@@ -283,7 +283,7 @@ frame.map(|frame| frame.start_address() + u64::from(addr.page_offset()))
bootloader = { version = "0.9.8", features = ["map_physical_memory"]}
```
この機能を有効化すると、ブートローダは物理メモリの全体を、ある未使用の仮想アドレス空間に対応付けます。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。
この機能を有効化すると、ブートローダは物理メモリの全体を、ある未使用の仮想アドレス空間にマッピングします。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。
### Boot Information
@@ -293,8 +293,8 @@ bootloader = { version = "0.9.8", features = ["map_physical_memory"]}
[`BootInfo`]: https://docs.rs/bootloader/0.9.3/bootloader/bootinfo/struct.BootInfo.html
[semver-incompatible]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements
- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリ対応付けはBIOSやUEFIファームウェアから取得できますが、それが可能なのはブートのごく初期に限られます。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリ対応付けは後で必要となります。
- `physical_memory_offset`は、物理メモリの対応付けの始まっている仮想アドレスです。このオフセットを物理アドレスに追加することによって、対応する仮想アドレスを得られます。これによって、カーネルから任意の物理アドレスにアクセスできます。
- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリマッピングはBIOSやUEFIファームウェアから取得できますが、それが可能なのはブートのごく初期に限られます。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリマッピングは後で必要となります。
- `physical_memory_offset`は、物理メモリのマッピングの始まっている仮想アドレスです。このオフセットを物理アドレスに追加することによって、対応する仮想アドレスを得られます。これによって、カーネルから任意の物理アドレスにアクセスできます。
ブートローダは`BootInfo`構造体を`_start`関数の`&'static BootInfo`引数という形でカーネルに渡します。この引数は私達の関数ではまだ宣言していなかったので追加します:
@@ -358,7 +358,7 @@ fn test_kernel_main(_boot_info: &'static BootInfo) -> ! {
## 実装
物理メモリへのアクセスができるようになったので、いよいよページテーブルのコードを実装できます。そのためにまず、現在有効な、私達のカーネルが使用しているページテーブルを見てみます。次に、与えられた仮想アドレスが対応付けられている物理アドレスを返す変換関数を作ります。最後に新しい対応付けを作るためにページテーブルを修正してみます。
物理メモリへのアクセスができるようになったので、いよいよページテーブルのコードを実装できます。そのためにまず、現在有効な、私達のカーネルが使用しているページテーブルを見てみます。次に、与えられた仮想アドレスがマップされている物理アドレスを返す変換関数を作ります。最後に新しいマッピングを作るためにページテーブルを修正してみます。
始める前に、`memory`モジュールを作ります:
@@ -388,7 +388,7 @@ use x86_64::{
///
/// この関数はunsafeである全物理メモリが、渡された
/// `physical_memory_offset`(だけずらしたうえ)で
/// 仮想メモリへと対応付けられていることを呼び出し元が
/// 仮想メモリへとマップされていることを呼び出し元が
/// 保証しなければならない。また、`&mut`参照が複数の
/// 名称を持つこと (mutable aliasingといい、動作が未定義)
/// につながるため、この関数は一度しか呼び出してはならない。
@@ -450,7 +450,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
![QEMU printing entry 0 (0x2000, PRESENT, WRITABLE, ACCESSED), entry 1 (0x894000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 31 (0x88e000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 175 (0x891000, PRESENT, WRITABLE, ACCESSED, DIRTY), and entry 504 (0x897000, PRESENT, WRITABLE, ACCESSED, DIRTY)](qemu-print-level-4-table.png)
いくつかの空でないエントリがあり、いずれも異なるレベル3テーブルに対応づけられていることがわかります。このようにたくさんの領域があるのは、カーネルコード、カーネルスタック、物理メモリ対応、ブート情報が互いに離れたメモリ領域を使っているためです。
いくつかの空でないエントリがあり、いずれも異なるレベル3テーブルにマップさせられていることがわかります。このようにたくさんの領域があるのは、カーネルコード、カーネルスタック、物理メモリマッピング、ブート情報が互いに離れたメモリ領域を使っているためです。
ページテーブルを更に辿りレベル3テーブルを見るには、エントリに対応するフレームを取り出し再び仮想アドレスに変換すればよいです
@@ -491,10 +491,10 @@ if !entry.is_unused() {
use x86_64::PhysAddr;
/// 与えられた仮想アドレスを対応する物理アドレスに変換し、
/// そのアドレスが対応付けられていないなら`None`を返す。
/// そのアドレスがマップされていないなら`None`を返す。
///
/// この関数はunsafeである。なぜなら、呼び出し元は全物理メモリが与えられた
/// `physical_memory_offset`(だけずらした上)で対応付けられていることを
/// `physical_memory_offset`(だけずらした上)でマップされていることを
/// 保証しなくてはならないからである。
pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr)
-> Option<PhysAddr>
@@ -554,9 +554,9 @@ fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr)
先程作った`active_level_4_table`関数を再利用せず、`CR3`レジスタからレベル4フレームを読み出すコードを再び書いています。これは簡単に試作するためであり、後でもっと良い方法で作り直すのでご心配なく。
`Virtaddr`構造体には、仮想メモリのインデクスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデクスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあとすなわちレベル1エントリを辿ったあとでは対応する物理フレームを指しています。
`Virtaddr`構造体には、(仮想メモリの)インデクスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデクスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあとすなわちレベル1エントリを辿ったあとでは対応する物理フレームを指しています。
ループの中では、前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームに対応付けられていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageに対応付けられていたら、今のところはpanicすることにします。
ループの中では、前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームにマップされていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageにマップされていたら、今のところはpanicすることにします。
[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame
@@ -580,7 +580,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
0x201008,
// スタックページのどこか
0x0100_0020_1a10,
// 物理アドレス "0" に対応付けられている仮想アドレス
// 物理アドレス "0" にマップされている仮想アドレス
boot_info.physical_memory_offset,
];
@@ -598,19 +598,19 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, "panicked at 'huge pages not supported'](qemu-translate-addr.png)
期待したとおり、恒等対応しているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダがカーネルの初期対応づけをどのようにつくったかによります。また、下から12ビットは変換のあとも常に同じであるということも注目に値しますこの部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。
期待したとおり、恒等マップしているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダがカーネルの初期マッピングをどのようにつくったかによります。また、下から12ビットは変換のあとも常に同じであるということも注目に値しますこの部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。
[_page offset_]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu
それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を変換すると物理アドレス`0`を指すはずです。しかし、効率よく対応付けを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。
それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を変換すると物理アドレス`0`を指すはずです。しかし、効率よくマッピングを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。
### `OffsetPageTable`を使う
仮想アドレスから物理アドレスへの変換はOSのカーネルがよく行うことですから、`x86_64`クレートはそのための抽象化を提供しています。この実装はすでにhuge pageや`translate_addr`以外の様々な関数もサポートしているので、以下ではhuge pageのサポートを自前で実装する代わりにこれを使うことにします。
この抽象化の基礎となっているのは、様々なページテーブル対応付け関数を定義している2つのトレイトです。
この抽象化の基礎となっているのは、様々なページテーブルマッピング関数を定義している2つのトレイトです。
- [`Mapper`]トレイトはページサイズを型引数とする<ruby>汎用型<rp> (</rp><rt>ジェネリクス</rt><rp>) </rp></ruby>で、ページに対して操作を行う関数を提供します。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しい対応付けを作成します。
- [`Mapper`]トレイトはページサイズを型引数とする<ruby>汎用型<rp> (</rp><rt>ジェネリクス</rt><rp>) </rp></ruby>で、ページに対して操作を行う関数を提供します。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しいマッピングを作成します。
- [`Translate`] トレイトは[`translate_addr`]や一般の[`translate`]のような、さまざまなページサイズに対して動くような関数を提供します。
[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html
@@ -620,13 +620,13 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
[`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr
[`translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate
これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスに対応していることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが(そのフレームから)計算可能な仮想アドレスに対応していることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。
これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスにマップしていることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが(そのフレームから)計算可能な仮想アドレスにマップしていることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。
[`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html
[`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html
[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html
私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリに対応付けているので、`OffsetPageTable`型が使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう:
私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリにマップしているので、`OffsetPageTable`型が使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう:
```rust
use x86_64::structures::paging::OffsetPageTable;
@@ -635,7 +635,7 @@ use x86_64::structures::paging::OffsetPageTable;
///
/// この関数はunsafeである全物理メモリが、渡された
/// `physical_memory_offset`(だけずらしたうえ)で
/// 仮想メモリへと対応付けられていることを呼び出し元が
/// 仮想メモリへとマップされていることを呼び出し元が
/// 保証しなければならない。また、`&mut`参照が複数の
/// 名称を持つこと (mutable aliasingといい、動作が未定義)
/// につながるため、この関数は一度しか呼び出してはならない。
@@ -650,7 +650,7 @@ unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
{}
```
この関数は`physical_memory_offset`を引数としてとり、`'static`ライフタイムを持つ`OffsetPageTable`を作って返します。このライフタイムは、私達のカーネルが実行している間この<ruby>実体<rp> (</rp><rt>インスタンス</rt><rp>) </rp></ruby>はずっと有効であるという意味です。関数の中ではまず`active_level_4_table`関数を呼び出し、レベル4ページテーブルへの可変参照を取得します。次に[`OffsetPageTable::new`]関数をこの参照を使って呼び出します。この`new`関数の第二引数には、物理メモリの対応付けの始まる仮想アドレスが入ることになっています。つまり`physical_memory_offset`です。
この関数は`physical_memory_offset`を引数としてとり、`'static`ライフタイムを持つ`OffsetPageTable`を作って返します。このライフタイムは、私達のカーネルが実行している間この<ruby>実体<rp> (</rp><rt>インスタンス</rt><rp>) </rp></ruby>はずっと有効であるという意味です。関数の中ではまず`active_level_4_table`関数を呼び出し、レベル4ページテーブルへの可変参照を取得します。次に[`OffsetPageTable::new`]関数をこの参照を使って呼び出します。この`new`関数の第二引数には、物理メモリのマッピングの始まる仮想アドレスが入ることになっています。つまり`physical_memory_offset`です。
[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new
@@ -691,24 +691,24 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, 0x18000000000 -> 0x0](qemu-mapper-translate-addr.png)
想定通り、`0xb8000`やコード・スタックアドレスの変換結果は自前の変換関数と同じになっています。また、`physical_memory_offset`は物理アドレス`0x0`対応付けられているのもわかります。
想定通り、`0xb8000`やコード・スタックアドレスの変換結果は自前の変換関数と同じになっています。また、`physical_memory_offset`は物理アドレス`0x0`マップされているのもわかります。
`MappedPageTable`型の変換関数を使うことで、huge pageをサポートする手間が省けます。また`map_to`のような他のページング関数も利用でき、これは次のセクションで使います。
この時点で、自作した`memory::translate_addr`関数や`memory::translate_addr_inner`関数はもう必要ではないので削除して構いません。
### 新しい対応を作る
### 新しいマッピングを作る
これまでページテーブルを見てきましたが、それに対する変更は行っていませんでした。ページテーブルに対する変更として、対応のなかったページに対応を作ってみましょう。
これまでページテーブルを見てきましたが、それに対する変更は行っていませんでした。ページテーブルに対する変更として、マッピングのなかったページにマッピングを作ってみましょう。
これを実装するには[`Mapper`]トレイトの[`map_to`]関数を使うので、この関数について少し見てみましょう。ドキュメントによると四つ引数があります:対応に使うページ、ページを対応させるフレーム、ページテーブルエントリにつかうフラグの集合、そして`frame_allocator`です。<ruby>フレームアロケータ<rp> (</rp><rt>frame allocator</rt><rp>) </rp></ruby>(フレームを<ruby>割り当てる<rp> (</rp><rt>アロケートする</rt><rp>) </rp></ruby>機能を持つ)が必要な理由は、与えられたページを対応付けるために追加でページテーブルを作成する必要があるかもしれず、これを格納するためには使われていないフレームが必要となるからです。
これを実装するには[`Mapper`]トレイトの[`map_to`]関数を使うので、この関数について少し見てみましょう。ドキュメントによると四つ引数があります:マッピングに使うページ、ページをマップさせるフレーム、ページテーブルエントリにつかうフラグの集合、そして`frame_allocator`です。<ruby>フレームアロケータ<rp> (</rp><rt>frame allocator</rt><rp>) </rp></ruby>(フレームを<ruby>割り当てる<rp> (</rp><rt>アロケートする</rt><rp>) </rp></ruby>機能を持つ)が必要な理由は、与えられたページをマップするために追加でページテーブルを作成する必要があるかもしれず、これを格納するためには使われていないフレームが必要となるからです。
[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to
[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html
#### `create_example_mapping`関数
私達が実装していく最初のステップとして、`create_example_mapping`関数という、与えられた仮想ページを`0xb8000`すなわちVGAテキストバッファの物理フレームに対応付ける関数を作ってみましょう。このフレームを選んだ理由は、対応付けが正しくなされたかをテストするのが容易だからです:対応付けたページに書き込んで、それが画面に現れるか確認するだけでよいのですから。
私達が実装していく最初のステップとして、`create_example_mapping`関数という、与えられた仮想ページを`0xb8000`すなわちVGAテキストバッファの物理フレームにマップする関数を作ってみましょう。このフレームを選んだ理由は、マッピングが正しくなされたかをテストするのが容易だからです:マッピングしたページに書き込んで、それが画面に現れるか確認するだけでよいのですから。
`create_example_mapping`は以下のようになります:
@@ -720,7 +720,7 @@ use x86_64::{
structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator}
};
/// 与えられたページをフレーム`0xb8000`に試しに対応付ける。
/// 与えられたページをフレーム`0xb8000`に試しにマップする。
pub fn create_example_mapping(
page: Page,
mapper: &mut OffsetPageTable,
@@ -739,20 +739,20 @@ pub fn create_example_mapping(
}
```
この関数は、対応付け`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するならトレイト引数のサイズが4KiBでも2MiBや1GiBのhuge pageでも構わない<ruby>汎用<rp> (</rp><rt>ジェネリック</rt><rp>) </rp></ruby>トレイトです。私達は4KiBの対応付けのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。
この関数は、マップす`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するならトレイト引数のサイズが4KiBでも2MiBや1GiBのhuge pageでも構わない<ruby>汎用<rp> (</rp><rt>ジェネリック</rt><rp>) </rp></ruby>トレイトです。私達は4KiBのマッピングのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。
[impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
[generic]: https://doc.rust-lang.org/book/ch10-00-generics.html
[`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html
[`PageSize`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/trait.PageSize.html
[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度対応付けると例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでに対応付けられているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数であり、この記事のあとには取り除かれるので大丈夫です。この危険性のことを忘れないようにするために、その行に<ruby>`FIXME`<rp> (</rp><rt>`要修正`</rt><rp>) </rp></ruby>コメントをつけておきます。
[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度マップすると例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでにマップされているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数であり、この記事のあとには取り除かれるので大丈夫です。この危険性のことを忘れないようにするために、その行に<ruby>`FIXME`<rp> (</rp><rt>`要修正`</rt><rp>) </rp></ruby>コメントをつけておきます。
`map_to`関数が`page``unused_frame`に加えてフラグの集合と`frame_allocator`への参照を取りますが、これについてはすぐに説明します。フラグについては、`PRESENT`フラグという有効なエントリ全てに必須のフラグと、`WRITABLE`フラグという対応するページを書き込み可能にするフラグをセットしています。フラグの一覧については、前記事の[ページテーブルの形式][_Page Table Format_]を参照してください。
[_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburunoxing-shi
[`map_to`]関数は失敗しうるので、[`Result`]を返します。これは失敗しても構わない単なるテストコードなので、エラーが起きたときは[`expect`]を使ってパニックしてしまうことにします。この関数は成功したとき[`MapperFlush`]型を返します。この型の[`flush`]メソッドを使うと、新しく対応させたページをトランスレーション・ルックアサイド・バッファ (TLB) から簡単にflushすることができます。この型は`Result`と同じく[`#[must_use]`][must_use]属性を使っており、使用し忘れると警告を出します。
[`map_to`]関数は失敗しうるので、[`Result`]を返します。これは失敗しても構わない単なるテストコードなので、エラーが起きたときは[`expect`]を使ってパニックしてしまうことにします。この関数は成功したとき[`MapperFlush`]型を返します。この型の[`flush`]メソッドを使うと、新しくマッピングしたページをトランスレーション・ルックアサイド・バッファ (TLB) から簡単にflushすることができます。この型は`Result`と同じく[`#[must_use]`][must_use]属性を使っており、使用し忘れると警告を出します。
[`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html
[`expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect
@@ -764,7 +764,7 @@ pub fn create_example_mapping(
`create_example_mapping`関数を呼べるようにするためには、まず`FrameAllocator`トレイトを実装する型を作成する必要があります。上で述べたように、このトレイトは新しいページのためのフレームを`map_to`が必要としたときに割り当てる役割を持っています。
単純なケースを考えましょう:新しいページテーブルを作る必要がないと仮定してしまいます。この場合、常に`None`を返すフレームアロケータで十分です。私達の対応付け関数をテストするために、そのような`EmptyFrameAllocator`を作ります。
単純なケースを考えましょう:新しいページテーブルを作る必要がないと仮定してしまいます。この場合、常に`None`を返すフレームアロケータで十分です。私達のマッピング関数をテストするために、そのような`EmptyFrameAllocator`を作ります。
```rust
// in src/memory.rs
@@ -779,30 +779,30 @@ unsafe impl FrameAllocator<Size4KiB> for EmptyFrameAllocator {
}
```
`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、実装したアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームに対応付けられたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator``None`しか返さないので、これは問題ではありません。
`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、実装したアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームにマップされたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator``None`しか返さないので、これは問題ではありません。
#### 仮想ページを選ぶ
`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、対応を作る際に追加のページテーブルフレームが必要でなかったときにのみうまく動作します。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう:
`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、マッピングを作る際に追加のページテーブルフレームが必要でなかったときにのみうまく動作します。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう:
![A virtual and a physical address space with a single mapped page and the page tables of all four levels](required-page-frames-example.svg)
この図の左は仮想アドレス空間を、右は物理アドレス空間を、真ん中はページテーブルを示します。このページテーブルが格納されている物理フレームが破線で示されています。仮想アドレス空間は一つの対応付けられたページをアドレス`0x803fe00000`に持っており、これは青色で示されています。このページをフレームに変換するために、CPUは4層のページテーブルを辿り、アドレス36KiBのフレームに到達します。
この図の左は仮想アドレス空間を、右は物理アドレス空間を、真ん中はページテーブルを示します。このページテーブルが格納されている物理フレームが破線で示されています。仮想アドレス空間は一つのマップされたページをアドレス`0x803fe00000`に持っており、これは青色で示されています。このページをフレームに変換するために、CPUは4層のページテーブルを辿り、アドレス36KiBのフレームに到達します。
また、この図はVGAテキストバッファの物理フレームを赤色で示しています。私達の目的は、`create_example_mapping`関数を使ってまだ対応付けられていない仮想ページをこのフレームに対応付けることです。私達の`EmptyFrameAllocator`は常に`None`を返すので、アロケータからフレームを追加する必要がないように対応付けを作りたいです。これができるかは、私達が対応付けにどの仮想ページを使うかに依存します。
また、この図はVGAテキストバッファの物理フレームを赤色で示しています。私達の目的は、`create_example_mapping`関数を使ってまだマップされていない仮想ページをこのフレームにマップすることです。私達の`EmptyFrameAllocator`は常に`None`を返すので、アロケータからフレームを追加する必要がないようにマッピングを作りたいです。これができるかは、私達がマッピングにどの仮想ページを使うかに依存します。
この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)対応付けられているページの3つ前です。レベル4と3のテーブルのインデクスは青いページと同じですが、レベル2と1のインデクスは違います[前の記事][page-table-indices]を参照。レベル2テーブルのインデクスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加でアロケートする必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。
この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)マップされているページの3つ前です。レベル4と3のテーブルのインデクスは青いページと同じですが、レベル2と1のインデクスは違います([前の記事][page-table-indices]を参照。レベル2テーブルのインデクスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加でアロケートする必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。
[page-table-indices]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu
まとめると、新しい対応を作るときの難易度は、対応付けようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリをそのページに一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル321のページテーブルを新しく作る必要があります。
まとめると、新しいマッピングを作るときの難易度は、マッピングしようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリをそのページに一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル321のページテーブルを新しく作る必要があります。
`EmptyFrameAllocator`を使って`create_example_mapping`を呼び出すためには、すべての階層のページテーブルがすでに存在しているページを選ぶ必要があります。そんなページを探すにあたっては、ブートローダが自分自身を仮想アドレス空間の最初の1メガバイトに読み込んでいるということを利用できます。つまり、この領域のすべてのページについて、レベル1テーブルがきちんと存在しているということです。したがって、試しに対応を作るときに、このメモリ領域のいずれかの未使用ページ、例えばアドレス`0`を使えばよいです。普通このページは、ヌルポインタの参照外しがページフォルトを引き起こすことを保証するために使用しないので、ブートローダもここを対応させてはいないはずです。
`EmptyFrameAllocator`を使って`create_example_mapping`を呼び出すためには、すべての階層のページテーブルがすでに存在しているページを選ぶ必要があります。そんなページを探すにあたっては、ブートローダが自分自身を仮想アドレス空間の最初の1メガバイトに読み込んでいるということを利用できます。つまり、この領域のすべてのページについて、レベル1テーブルがきちんと存在しているということです。したがって、試しにマッピングを作るときに、このメモリ領域のいずれかの未使用ページ、例えばアドレス`0`を使えばよいです。普通このページは、ヌルポインタの参照外しがページフォルトを引き起こすことを保証するために使用しないので、ブートローダもここをマップさせてはいないはずです。
#### 対応を作る
#### マッピングを作る
というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`対応付けるよう`kernel_main`関数を変更していきましょう。このページをVGAテキストバッファのフレームに対応付けると、以後、画面に書き込むことができるようになるはずです。実装は以下のようになります:
というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`マップするよう`kernel_main`関数を変更していきましょう。このページをVGAテキストバッファのフレームにマップすると、以後、画面に書き込むことができるようになるはずです。実装は以下のようになります:
```rust
// in src/main.rs
@@ -817,11 +817,11 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
let mut mapper = unsafe { memory::init(phys_mem_offset) };
let mut frame_allocator = memory::EmptyFrameAllocator;
// 未使用のページを対応付け
// 未使用のページをマップす
let page = Page::containing_address(VirtAddr::new(0));
memory::create_example_mapping(page, &mut mapper, &mut frame_allocator);
// 新しい対応付けを使って、文字列`New!`を画面に書き出す
// 新しいマッピングを使って、文字列`New!`を画面に書き出す
let page_ptr: *mut u64 = page.start_address().as_mut_ptr();
unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)};
@@ -829,7 +829,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
}
```
まず、`mapper``frame_allocator`インスタンスの可変参照を渡して`create_example_mapping`を呼ぶことで、アドレス`0`のページに対応を作っています。これはVGAテキストバッファのフレームに対応付けているので、これに書き込んだものは何であれ画面に出てくるはずです。
まず、`mapper``frame_allocator`インスタンスの可変参照を渡して`create_example_mapping`を呼ぶことで、アドレス`0`のページにマッピングを作っています。これはVGAテキストバッファのフレームにマップしているので、これに書き込んだものは何であれ画面に出てくるはずです。
次にページを生ポインタに変更して、オフセット`400`に値を書き込みます。このページの最初に書き込むとVGAバッファの一番上の行になり、次のprintlnで即座に画面外に流れていってしまうので、それを避けています。値`0x_f021_f077_f065_f04e`は、白背景の"New!"という文字列を表します。[VGAテキストモードの記事][in the _“VGA Text Mode”_ post]で学んだように、VGAバッファへの書き込みはvolatileでなければならないので、[`write_volatile`]メソッドを使っています。
@@ -840,9 +840,9 @@ QEMUで実行すると、以下の出力を得ます
![QEMU printing "It did not crash!" with four completely white cells in the middle of the screen](qemu-new-mapping.png)
画面の "New!" はページ`0`への書き込みによるものなので、ページテーブルへの新しい対応付けの作成が成功したということを意味します。
画面の "New!" はページ`0`への書き込みによるものなので、ページテーブルへの新しいマッピングの作成が成功したということを意味します。
この対応付けが成功したのは、アドレス`0`を管轄するレベル1テーブルがすでに存在していたからに過ぎません。レベル1テーブルがまだ存在しないページを対応付けようとすると、`map_to`関数は新しいページテーブルを作るために`EmptyFrameAllocator`からフレームを割り当てようとしてエラーになります。`0`の代わりに`0xdeadbeaf000`対応付けようとするとそれが発生するのが見られます。
このマッピングが成功したのは、アドレス`0`を管轄するレベル1テーブルがすでに存在していたからに過ぎません。レベル1テーブルがまだ存在しないページをマッピングしようとすると、`map_to`関数は新しいページテーブルを作るために`EmptyFrameAllocator`からフレームを割り当てようとしてエラーになります。`0`の代わりに`0xdeadbeaf000`マッピングしようとするとそれが発生するのが見られます。
```rust
// in src/main.rs
@@ -860,7 +860,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5
```
レベル1テーブルがまだ存在していないページを対応付けるためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう?
レベル1テーブルがまだ存在していないページをマップするためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう?
### フレームを割り当てる
@@ -930,7 +930,7 @@ impl BootInfoFrameAllocator {
この関数はイテレータのコンビネータメソッドを使って、最初に与えられる`MemoryMap`を使用可能な物理フレームのイテレータに変換します:
- まず`iter`メソッドを使ってメモリマップを[`MemoryRegion`]のイテレータに変える。
- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作った対応付けに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに<ruby>`InUse`<rp> (</rp><rt>`使用中`</rt><rp>) </rp></ruby>などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。
- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作ったマッピングに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに<ruby>`InUse`<rp> (</rp><rt>`使用中`</rt><rp>) </rp></ruby>などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。
- つぎに、[`map`]コンビネータとRustの[range構文][range syntax]を使って、メモリ領域のイテレータからアドレス範囲のイテレータへと変換する。
- つぎに、アドレス範囲から[`step_by`]で4096個ごとにアドレスを選び、[`flat_map`]を使うことでフレームの最初のアドレスのイテレータを得る。4096バイト4KiBはページのサイズに等しいので、それぞれのフレームの開始地点のアドレスが得られます。ブートローダのページは使用可能なメモリ領域をすべてアラインするので、ここで改めてアラインや丸めを行う必要はありません。`map`ではなく[`flat_map`]を使うことで、`Iterator<Item = Iterator<Item = u64>>`ではなく`Iterator<Item = u64>`を得ています。
- 最後に、開始アドレスの型を`PhysFrame`に変更することで`Iterator<Item = PhysFrame>`を得ている。
@@ -989,24 +989,24 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
}
```
ブート情報を使うフレームアロケータのおかげで対応付けは成功し、白背景に黒文字の"New!"が再び画面に現れました。舞台裏では、`map_to`メソッドが不足しているページテーブルを以下のやり方で作っています:
ブート情報を使うフレームアロケータのおかげでマッピングは成功し、白背景に黒文字の"New!"が再び画面に現れました。舞台裏では、`map_to`メソッドが不足しているページテーブルを以下のやり方で作っています:
- 渡された`frame_allocator`を使って未使用のフレームを割り当ててもらう。
- フレームをゼロで埋めることで、新しい空のページテーブルを作る。
- 上位のテーブルのエントリをそのフレームに対応付ける。
- 上位のテーブルのエントリをそのフレームにマップする。
- 次の層で同じことを続ける。
`create_example_mapping`関数はただのお試しコードにすぎませんが、今や私達は任意のページに対応付けを作れるようになりました。これは、今後の記事で行うメモリ割り当てやマルチスレッディングにおいて不可欠です。
`create_example_mapping`関数はただのお試しコードにすぎませんが、今や私達は任意のページにマッピングを作れるようになりました。これは、今後の記事で行うメモリ割り当てやマルチスレッディングにおいて不可欠です。
[](#create-example-mappingguan-shu)で説明したような未定義動作を誤って引き起こしてしまうことのないよう、この時点で`create_example_mapping`関数を再び取り除いておきましょう。
## まとめ
この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等対応、物理メモリ全体の対応付け、一時的な対応、再帰的ページテーブルなどです。このうち、シンプルで<ruby>ポータブル<rp> (</rp><rt>アーキテクチャ非依存</rt><rp>) </rp></ruby>で強力な、物理メモリ全体の対応付けを選びました。
この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等マップ、物理メモリ全体のマッピング、一時的なマッピング、再帰的ページテーブルなどです。このうち、シンプルで<ruby>ポータブル<rp> (</rp><rt>アーキテクチャ非依存</rt><rp>) </rp></ruby>で強力な、物理メモリ全体のマッピングを選びました。
ページテーブルにアクセスできなければ物理メモリを対応付けられないので、ブートローダの補助が必要でした。`bootloader`クレートはcargoのfeaturesというオプションを通じて、必要となる対応付けの作成をサポートしています。さらに、必要となる情報をエントリポイント関数の`&BootInfo`引数という形で私達のカーネルに渡してくれます。
ページテーブルにアクセスできなければ物理メモリをマップされないので、ブートローダの補助が必要でした。`bootloader`クレートはcargoのfeaturesというオプションを通じて、必要となるマッピングの作成をサポートしています。さらに、必要となる情報をエントリポイント関数の`&BootInfo`引数という形で私達のカーネルに渡してくれます。
実装について最初はページテーブルを辿る変換関数を自分の手で実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しい対応を作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。
実装についてですが、最初はページテーブルを辿る変換関数を自分の手で実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しいマッピングを作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。
## 次は?