panda's tech note

Advent Calendar 2018: advos

Day 19: メモリ管理(設計・ページテーブル)

昨日までは物理メモリ管理をしました。今日からはカーネルメモリ管理(仮想メモリと物理メモリの両方)を取り扱おうと思います。ここまでは,ACPI用のメモリなど,カーネルメモリとして物理メモリを直接使ってきましたが,そこも正しく仮想メモリを使うように書き換えていきます。仮想メモリを扱う前に,今日はメモリ管理の設計方針を説明しようと思います。

メモリ管理の実装上の難しさ

物理メモリ管理のときに,「メモリ管理をするためのメモリが必要になる」ため,管理構造体を1 MiB以下の領域に用意しました。仮想メモリの管理まで含めると,このメモリ管理がさらに複雑になります。以下に依存関係をまとめました。

  • 仮想メモリ→物理メモリ:仮想メモリを管理する構造体を割り当てるには物理メモリと仮想メモリが必要
  • ページテーブル→物理メモリ:仮想メモリを割り当てるには物理メモリを使ったページテーブル設定が必要
  • ページテーブル→仮想メモリ:ページテーブルを設定するためには,ページテーブルに使う物理ページに対して仮想メモリを設定し,仮想メモリ上でページテーブル設定をする必要がある

このように依存関係がループするため,汎用的なコードを書くとコードの視認性や品質が下がります。そのため,今回は以下のアプローチを取ります。

メモリ管理の実装アプローチ

  • 物理ページ:前述した通り,バディシステムは,物理メモリの全空間を仮想メモリにリニアマッピングして取り扱います。仮想メモリは0x100000000(4 GiB)から上の領域にマッピングします。この領域は特権からのみアクセス可能とします。
  • カーネルページテーブル:カーネルのページテーブルは上述したメモリ管理実装の依存関係を取り除くため,1 GiB分(3-4 GiBの領域)のページテーブルを予約しておきます(4 KiB * 262144 entries: 512 4 KiB Page Tables + PD, PDPT, PML4)。
  • カーネル仮想ページ:カーネルの仮想ページは上記のページテーブルと同じく,1 GiB分(262144ページ分)予約します。具体的な管理構造体は後日説明します。
  • ユーザランド仮想ページ:カーネルページテーブル,カーネル仮想ページおよび物理ページ管理が実装できれば,カーネルのメモリ管理が実装できるので,それを使ってユーザランドの仮想ページを管理します。

ページテーブルの管理

仮想ページと物理ページのマッピングはページテーブルにより設定します。まず,ページテーブルの管理を実装します。

ページテーブルのエントリは全て物理アドレスでの指定を行います。一方,ページテーブルのエントリを書き換えるには仮想メモリで行う必要があります。0x100000000から上の領域にすべての物理メモリ領域をリニアマッピングをしているので,これを使って仮想メモリとしてアクセスすることができます。ここでは,物理ページに対応付けられたこのリニアマッピングを使って,仮想ページ管理とページテーブル管理の相互依存を解消させます。

src/kernel/arch/x86_64/pgt.cにページテーブルを設定するコードをまとめました。

int
pgt_map(pgt_t *pgt, uintptr_t virtual, uintptr_t physical, int superpage,
        int global, int rw, int user)

で,virtualから始まるページをphysicalにマップする設定を追加します。globalrwuserはページテーブルのG(他のページテーブルと共有するエントリ),R/W(読み書き可のエントリ),U/S(特権なしのアクセスを許可)フラグです。

また,

int
pgt_unmap(pgt_t *pgt, uintptr_t virtual, int superpage)

で仮想アドレスvirtualに割り当てられているページのマッピングを解除します。

上述した通り,ページテーブルの設定にはページテーブルのメモリが必要です。ここでは,

void
pgt_push(pgt_t *pgt, pgt_entry_t *pg)

で,ページテーブルに使用するページを追加します。

カーネルのベースページテーブル(一時ページテーブル)の設定

src/kernel/arch/x86_64/pgt.cの実装を利用して,setup_kernel_pgt()でアドレスを直打ちしていたカーネルの初期ページテーブルを作り直します。この時点ではまだ0x100000000からのリニアマッピング設定ができていないため,これまでと同様に以下の表の0x00069000-0x0006ffffのリニアマッピング領域を使います。

Start End Description
00000500 00007bff ブートローダのスタック領域
00007c00 00007dff MBR
00007e00 00007fff 未使用
00008000 00008fff ブート情報(ドライブ番号など)
00009000 0000cfff ブートモニタ (16 KiB)
0000d000 0000ffff 未使用 (ブートモニタ拡張用に予約)
00010000 0002ffff カーネル (最大128 KiB)
00030000 00067fff 予約
00060000 0006ffff グローバル変数領域
00068000 00068fff コアゾーン管理用領域
00069000 0006ffff カーネルのベースページテーブル
00070000 00073fff trampoline (16 KiB)
00074000 00075fff GDT (8 KiB)
00076000 00077fff IDT (8 KiB)
00078000 00078fff 予約
00079000 0007ffff ロングモード用ページテーブル (24 KiB = 6 * 4 KiB以上必要)

setup_kernel_pgt()の内容は以下のように書き換えられます。

    /* Setup and enable the kernel page table */
    pgt_init(&tmppgt, (void *)0x69000, 0);
    kmemset((void *)0x69000, 0, 4096);
    for ( i = 1; i < 6; i++ ) {
        pgt_push(&tmppgt, (void *)0x69000 + 4096 * i);
    }
    /* 0-1 GiB */
    for ( i = 0; i < 512; i++ ) {
        pgt_map(&tmppgt, i * MEMORY_SUPERPAGESIZE, i * MEMORY_SUPERPAGESIZE,
                1, 0, 1, 0);
    }
    /* 3-4 GiB (first 2 and the tail MiB) */
    for ( i = 0; i < 1; i++ ) {
        pgt_map(&tmppgt, (uintptr_t)KERNEL_RELOCBASE + i * MEMORY_SUPERPAGESIZE,
                i * MEMORY_SUPERPAGESIZE, 1, 0, 1, 0);
    }
    for ( i = 502; i < 512; i++ ) {
        pgt_map(&tmppgt, (uintptr_t)KERNEL_RELOCBASE + i * MEMORY_SUPERPAGESIZE,
                (uintptr_t)KERNEL_RELOCBASE + i * MEMORY_SUPERPAGESIZE,
                1, 0, 1, 0);
    }
    /* 4-5 GiB (first 64 MiB) */
    for ( i = 0; i < 32; i++ ) {
        pgt_map(&tmppgt, (uintptr_t)KERNEL_LMAP + i * MEMORY_SUPERPAGESIZE,
                i * MEMORY_SUPERPAGESIZE, 1, 0, 1, 0);
    }
    pgt_set_cr3(&tmppgt);

カーネルページテーブル

上記のページテーブルで,0-64 MiBの領域を0x100000000からの仮想メモリにリニアマッピングしたので,これで物理メモリが使用できるようになりました。この領域を利用して,コアゾーンの物理メモリ領域を初期化した後,物理メモリ全体を0x100000000からの仮想メモリにリニアマッピングするページテーブルを作成します。今回は,この仮想ページを

/*
 * Initialize the kernel page table
 */
static int
_init_kernel_pgt(kvar_t *kvar, size_t nr, memory_sysmap_entry_t *map)
{
    size_t i;
    uintptr_t addr;
    uintptr_t maxaddr;
    size_t npg;
    void *pages;

    /* Get the maximum address of the system memory */
    maxaddr = 0;
    for ( i = 0; i < nr; i++ ) {
        addr = map[i].base + map[i].len;
        if ( addr > maxaddr ) {
            maxaddr = addr;
        }
    }
    /* # of pages */
    npg = ((maxaddr + 0x1fffff) >> 21);

    /* Allocate 512 pages for page tables */
    pages = phys_mem_buddy_alloc(kvar->phys.czones[MEMORY_ZONE_KERNEL].heads,
                                 9);
    if ( NULL == pages ) {
        return -1;
    }

    /* Initialize the kernel page table */
    pgt_init(&kvar->pgt, pages, KERNEL_LMAP);
    for ( i = 1; i < (1 << 9); i++ ) {
        pgt_push(&kvar->pgt, pages + i * 4096);
    }

    /* 0-1 GiB */
    for ( i = 0; i < 512; i++ ) {
        pgt_map(&kvar->pgt, i * MEMORY_SUPERPAGESIZE, i * MEMORY_SUPERPAGESIZE,
                1, 0, 1, 0);
    }
    /* 3-4 GiB (first 2 and the tail MiB) */
    for ( i = 0; i < 1; i++ ) {
        pgt_map(&kvar->pgt,
                (uintptr_t)KERNEL_RELOCBASE + i * MEMORY_SUPERPAGESIZE,
                i * MEMORY_SUPERPAGESIZE, 1, 0, 1, 0);
    }
    for ( i = 502; i < 512; i++ ) {
        pgt_map(&kvar->pgt,
                (uintptr_t)KERNEL_RELOCBASE + i * MEMORY_SUPERPAGESIZE,
                (uintptr_t)KERNEL_RELOCBASE + i * MEMORY_SUPERPAGESIZE,
                1, 0, 1, 0);
    }

    /* Linear mapping */
    for ( i = 0; i < npg; i++ ) {
        pgt_map(&kvar->pgt, (uintptr_t)KERNEL_LMAP + i * MEMORY_SUPERPAGESIZE,
                i * MEMORY_SUPERPAGESIZE, 1, 0, 1, 0);
    }

    /* Activate the page table */
    pgt_set_cr3(&kvar->pgt);

    return 0;
}

まとめと明日の予定

これまでハードコーディングで行ってきたページテーブルの設定を,リニアマッピングした物理ページを用いることでページテーブルの設定を簡単に行えるようにしました。明日はこれを使って仮想ページの管理とページアロケータを実装します。