起動するまでの長い道のり メモリ管理編(1) ページング概要の巻

 またしても再開……。

 さて、D言語が普通に動かせるようになった。scope指定をすればクラス・オブジェクトも作れるようになった。
 が、まだ動的なメモリ確保やオブジェクト作成ができない。もっと言えば、実行時にサイズが初めて確定するようなデータのメモリ領域を確保できない。
 動的なメモリ確保を行うためには、メモリ領域を色々と管理しなければならない。どの領域がどのくらい空いているのか、あるいは使われているのか、常に把握しておく必要がある。
 これから、とりあえずカーネル・モードでのメモリ管理について解説する。

ページングについて

 ページングとは、メモリ管理の方法の一つだ。
 少し前にセグメンテーションというメモリを分割する機能のことを説明した。ページングも同じようにメモリを分割する。ただ、ページングではメモリを固定サイズのページに区切る。具体的に言うと4KBで区切る(実は4MBでも区切れたりするが割愛)。そして、メモリをページに区切った上で、各ページにアクセス権(読み書き可能かどうか等)などが設定できる。
 また、それぞれのページが実際に存在するかどうかも設定できたりする。とりあえず実際にメモリのある分までページを用意しておき、さらに、実メモリのない領域にもページがあることにしたりできる。ページはあるけれどメモリ上に実際にはない。
 そうしておくとどんないいことがあるのかというと、実際には存在しないメモリ領域へのアクセスを検知して、空いている実メモリをそこに割り当て、元の処理を続行させたりできる。これがいわゆるひとつの仮想メモリという奴だ。
 さらにページングでは、メモリ上にバラバラに存在しているページをまとめて一つの大きな塊にできたり、一つのページをあたかも複数の領域に存在しているかのように見せかけたりもできる。
 ページングは、もうセグメンテーション要らないかも、というくらい高機能だ。実際Linuxではセグメンテーションをほとんど使わずにページングを活用している。本ブログのOSもページングを主に使うことにする。

アドレス変換

 前節で、ページングを使うと実際にないメモリ領域にもメモリがあることにできると書いた。もっと具体的な言い方をすると、物理メモリのないアドレスへのアクセスを、物理メモリのあるアドレスへのアクセスに変換できる。
 たとえば、メモリが0x0000_ffffまでしか無いとしよう。そして0x1000_0000にアクセスが行われたとする。ページングがなければこれはエラーだ。しかし、ページングで適切にアドレスが変換されている場合、0x1000_0000へのアクセスが0x0000_1000へのアクセスに変換されたりする。これなら正常な処理として実行できる。
 先ほどちょっと書いた「バラバラのページをまとめる機能」や「一つのページを複数の領域にあるように見せる機能」は、このアドレス変換の仕組みを使って実現される。
 例えばアドレス0x0000_0fffと0x0000_1000は連続したアドレスだ。だけどページングによる変換を通すとそれぞれ0x0000_3fffと0x0000_8000になったりすることもある。これはかなり離れたところにある領域だ。普通のプログラムはアドレス変換後がどうなっているか意識しない。平坦な一つながりのメモリにしか見えない。つまり、実際に離れている領域が連続しているように見える。
「一つのページを複数の領域」では、アドレス0x0000_1000と0x0000_2000があったとして、その両方に0x0000_1000を割り当てることで実現できる。そうすると、プログラムが0x0000_1000に何かを書いた場合、0x0000_2000を読むと書いた内容がそのまま見える。逆もまた然りだ。これはいわゆる共有メモリだ。(UNIXのプロセスとかで使う共有メモリはちょっと違うが、コンセプトは大体同じ)
 さて、今までの話でアドレスには2種類あることが分かると思う。つまり変換前のアドレスと変換後のアドレスだ。変換前のアドレスのことをリニア・アドレスと言う。対する変換後のアドレスは物理アドレスと言う。このリニア・アドレス→物理アドレスの対応を設定するのが、ページングの中でも割と複雑で、だけど有用な機能だ。

アドレス変換の実際

 今までアドレスはアドレス、とにかくメモリの番地だった。ページングがOnになるとそれが変わる。アドレスがアドレスじゃなくなるのだ。
 ここではとりあえず32ビットのアドレスを使って最大4GBのメモリを取り扱う場合について解説する。今ではアドレスが64ビットだったり最大メモリだってテラバイト単位だったりするが、まあ基本はきっと同じだろう。
 ページングがOnになると、アドレスが3つに分割される(繰り返すが、32ビットで4GBメモリの場合)。上から10ビット・10ビット・12ビットで合計32ビットといった感じだ。分割してどうするのか、というと、それぞれのビット列を配列のインデックスと見なす。10ビットあれば0〜1023まで1024個の配列のインデックスを指定できる。12ビットあれば4096個だ。
 で、最初の10ビットはページ・ディレクトという配列のインデックスになる。次の10ビットはページ・テーブルという配列のインデックスだ。最後の12ビットはオフセットと呼ばれ、ページ・ディレクトリとページ・テーブルから割り出されたページ(サイズ4096バイト)の中の1バイトを指す。
 ページ・ディレクトリにはページ・テーブルの物理アドレスが、ページ・テーブルにはページの物理アドレスが入っている。アドレス変換を行う場合、ページ・ディレクトリからページ・テーブルを見つけ、そのページ・テーブルからページを見つけ、そうしてページのある1バイトにアクセスする。これはプログラマならコードで書くと分かりやすいと思う。正確な例ではないけど、イメージはつかめるだろう。

// このリニア・アドレスを物理アドレスに変換してみよう。
Pointer p = 0x0000_1000;

// ページ・ディレクトリからページ・テーブルを探す。
// 上位10ビットだけ使う。
table = directory[p >> 22];

// ページ・テーブルからページを探す。
// 真ん中の10ビットだけ使う。
page = table[(p >> 12) & 0x3ff];

// ページ内オフセット。最後の12ビット。
// これでバイトが特定できる。
byte = page[p & 0xfff];

 こんな3段階の変換を勝手にやってくれる。つくづくCPUは偉い。
 が、全部が全部勝手というわけではない。ページ・ディレクトリとページ・テーブルは自分で用意して設定する必要があるのだ。というかそういうところ自分で設定できないと困るよな……。

ページ・ディレクトリとページ・テーブル

 ページ・ディレクトリにはページ・テーブルの、ページ・テーブルにはページのアドレスが書いてあると言った。実はディレクトリやテーブルにはアドレスばかりではなく、ページが存在しているかどうかやアクセス権限などの情報も含まれている。
 ページ・ディレクトリやページ・テーブルを書いたり読んだりすることで、アドレス変換の設定だけでなく権限の制御や情報の取得もできる。

次回予告

 今日はこのくらいにしておこう……。次回はページ・ディレクトリとページ・テーブルの具体的な構造やページングのOn・Offの仕方とかを書こうかな。

参考資料

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

はじめて読む486―32ビットコンピュータをやさしく語る

はじめて読む486―32ビットコンピュータをやさしく語る