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

 前回でメモリのどこからどこまで使用可能か分かるようになった。今度は使用可能な範囲のページ・ディレクトリとページ・テーブルを用意していよいよページングをOnにしてみたいと思う。

今回のソース

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

 前々回あたりに説明したように、ページングによるアドレス変換はページ・ディレクトリとページ・テーブルを参照して行われる。ページ・ディレクトリにはページ・テーブルの物理アドレスが、ページ・テーブルにはページの物理アドレスがそれぞれ格納されている。リニア・アドレスが与えられた場合、まずページ・ディレクトリからページ・テーブルが検索され。次にページ・テーブルからページが検索される。
 ページ・ディレクトリとページ・テーブルはそれぞれ1024個の要素がある配列になっている。どちらも物理アドレスを格納した配列という点でとても良く似た構造をしている。ページ・ディレクトリの要素の事をページ・ディレクトリ・エントリ(略してPDE)、ページ・テーブルの要素の事をページ・テーブル・エントリ(略してPTE)と言う。この2つの構造は以下の通りだ。

ビット位置 意味
0 存在フラグ。対応するページが実際に存在するかどうか。ここが0になっていると、他の全ビットはCPUから無視される。そして、OSが自由に使用できる。
1 読み取り/書き込みフラグ。0の場合は読み取り専用。1の場合は書き込みも可能。
2 ユーザ/スーパーバイザ・フラグ。0の場合はスーパーバイザ(コード・セグメントの特権レベルが0〜2)のみアクセス可能。1の場合は誰でもアクセス可能となる。
3 ライトスルー・フラグ。0の場合、対応するメモリのキャッシュがライト・バック方式になる。1の場合、ライト・スルー方式となる。どう違うかは割愛。
4 キャッシュ・ディスエーブル・フラグ。0の場合、メモリのキャッシングが行われる。1の場合、キャッシングは行われなくなる。
5 アクセス・フラグ。0にしておくと、対応するメモリがアクセスされた時に1になる。どのページにアクセスがあったかが分かる。
6 PTEでは、ダーティ・フラグ。0にしておくと、対応するメモリに書き込みが行われた時に1になる。どのページに書き込みがあったかが分かる。PDEでは、予約ビットになっている。
7 PDEでは、ページ・サイズ・フラグ。0の場合、1ページのサイズは4KB。1の場合は4MB。PTEでは、ページ・テーブル属性インデックス。なんかPen3以降ではページの属性を別のテーブルで定義できるみたいだが使わないので割愛。
8 PTEでは、グローバル・ページ・フラグ。タスク・スイッチやページ・ディレクトリの変更が起きてもTLBがフラッシュされない。PDEでは、無視される。きっと使わないので詳細は割愛。
9〜11 OSで勝手に使用していいビット。
12〜31 PDEでは、ページ・テーブルの物理アドレスの上位20ビット。PTEでは、ページの物理アドレスの上位20ビット。

 PDEもPTEも、物理アドレスは必ず4096の倍数になる(しなければならない)。よって、物理アドレスの下位12ビットは必ず0になる。その必ず0になる部分に色々フラグを詰め込んだような構造になっている。だから、下位12ビットをマスクするとエントリの指している物理アドレスが得られる。
 今回はPDEとPTEをまとめて扱うPageEntryという構造体を定義した。それと、ページ・エントリの配列であるページ・ディレクトリ(PageDirectory)とページ・テーブル(PageTable)もaliasを定義しておいた。詳しくはpage.dを参照して欲しい。

ページ・テーブルの確保

 ページ・ディレクトリとページ・テーブル自身のためにかなりメモリが必要だ。ページ・ディレクトリは4KB、ページ・テーブルは4MBにつき4KBずつ必要だ。メモリが128MBあった場合、4KB + (128MB / 4MB) * 4KBで132KBも必要だ。因みにメモリが4GBある場合は4KB + 4MB必要だ。これはなかなかデカい。
 今回はメモリのサイズが分かっていたりするので、カーネルの後ろに直接確保するような事はしない。メモリマップからメモリのサイズを調べ、まだ使われていない高位のアドレスにページ・テーブルを格納することにする。

// page.d

/// ページングの初期化。
bool initializePaging() {
    // メモリサイズを得る。
    uint64_t end = 0;
    foreach(e; MEMORY_MAP) {
        if(e.type == MemoryMapEntry.Type.MEMORY && end < e.end) {
            end = e.end;
        }
    }
    
    // ページ・テーブル数を求める。
    auto tableCount = cast(size_t)(((end + ~PAGE_TABLE_MASK) & PAGE_TABLE_MASK) >> PAGE_TABLE_OFFSET);
    
    // メモリ量があまりにも少なかったらエラー。(1MB未満?)
    if(tableCount == 0) {
        return false;
    }
    
    // 1メガ以上のメモリが全ページ・テーブルに十分なだけ存在すれば、
    // 1メガ以上の部分にページ・テーブルを設定。
    // そうでなければカーネル末尾に設定。
    if(end > cast(uint32_t)(PAGE_DIRECTORY_HIGH + PAGE_LENGTH + tableCount * PAGE_LENGTH)) {
        directory_ = cast(PageDirectory*) PAGE_DIRECTORY_HIGH;
        tables_ = (cast(PageTable*)(directory_ + 1))[0 .. tableCount];
    } else {
        directory_ = cast(PageDirectory*) kernel_end;
        tables_ = (cast(PageTable*)(directory_ + 1))[0 .. tableCount];
    }

    /*** 後略 ***/
}

private:

/// ページ・ディレクトリ。
PageDirectory* directory_;

/// ページ・テーブル。
PageTable[] tables_;

 なんか色々複雑なことをやっている。でも単に、ページ・ディレクトリを指すポインタdirectory_と、ページ・テーブルの配列tables_を設定しているだけだったりする。ページ・ディレクトリの後に全メモリ分のページ・テーブルが並んでいるような形にしている。これで、directory_とtables_を使ってページ・テーブルを設定したりできる。ただ、まだページ・テーブルの内容は不確定だし、実際にページ・テーブルとして使われるようにもなっていない。

ページ・テーブルの初期化

 ページ・テーブルにアクセスできるようになったら、次は内容を設定する。

// page.d

    // ページ・ディレクトリ初期化。
    foreach(i, inout e; *directory_) {
        // 物理メモリの範囲内だった場合はページ・テーブルと対応付ける。
        if(i * PAGE_DIRECTORY_LENGTH < end) {
            e = PageEntry(tables_.ptr + i);
        } else {
            // 存在しない領域のエントリは0で初期化。
            e = PageEntry.init;
        }
    }
    
    // ページ・テーブル初期化。
    foreach(i, inout t; tables_) {
        foreach(j, inout e; t) {
            e = PageEntry(cast(void*)(i * PAGE_DIRECTORY_LENGTH + j * PAGE_LENGTH));
        }
    }

 foreachループでページ・ディレクトリとページ・テーブルの各エントリを設定している。ページ・ディレクトリにはページ・テーブルの物理アドレスを、ページ・テーブルにはメモリ上のページのアドレスを設定している。
 とりあえずリニア・アドレスと物理アドレスが同じになるようなマッピングを行っている。つまりページングがOnになってもリニア・アドレス = 物理アドレスになる。

ページングの開始

 ページ・テーブルの初期化が終わったのでいよいよページングをOnにする。ページング用のアセンブラ命令をラップした関数を定義し、それを呼び出す。

// page.d

/// ページング有効化。
void enablePaging() {
	asm {
		mov EAX, CR0;
		or EAX, 0x8000_0000;
		mov CR0, EAX;
	}
}

/// ページ・ディレクトリの設定。
void loadPageDirectory(PageDirectory* dir) {
	asm {
		mov EAX, dir;
		mov CR3, EAX;
	}
}

/// ページングの初期化。
bool initializePaging() {
    /*** 前略 ***/
    
    // ページ・ディレクトリのロード。
    loadPageDirectory(directory_);
    
    // ページングの開始。
    enablePaging();
    
    return true;
}

 これでinitializePagingをstartupから呼び出せば、めでたくページングがOnになるはずだ。サンプルコードでは合計ページ・テーブル数も表示するようにしてみた。メモリ容量 / 4MBの値になっているはずだ。

次回予告

 これでページングはOnにできたが、メモリ管理というにはまだ程遠い。次回以降は、どのページが使用されていてどのページが空いているのかを管理したり、ページの要求・解放などを行えるようにする。

参考資料

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

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

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