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
にマップする設定を追加します。global
,rw
,user
はページテーブルの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;
}
まとめと明日の予定
これまでハードコーディングで行ってきたページテーブルの設定を,リニアマッピングした物理ページを用いることでページテーブルの設定を簡単に行えるようにしました。明日はこれを使って仮想ページの管理とページアロケータを実装します。