ページテーブル
ページングやプロセスの設定も完了したので、ページテーブルを構築していきます。
ページテーブルでは仮想アドレスを物理アドレスに変換するための情報が格納されています。
仮想アドレスから物理アドレスへの変換を行えるようにすることで、プロセスごとに独立したメモリ空間を持つことができるようになります。
プロセスAとプロセスBで同じ0x00001111という仮想メモリアドレスがある場合でも、ページテーブルを参照すれば別々の物理アドレスを参照できるようになります。
RISCVのページング機構
RISCVではいくつかのページング機構がドキュメントで定義されています。
今回はRISC-Vドキュメント(The RISC-V Instruction Set Manual Volume Ⅱ)の11.3で定義されている32bit用ページング機構Sv32に則って構築していきます。
RISCVのページング機構の仕組みは以下サイトがよく参考になりました。
https://blog.rogiken.org/blog/2023/03/05/risc-v%e3%81%ab%e3%81%8a%e3%81%91%e3%82%8b%e4%bb%ae%e6%83%b3-%e7%89%a9%e7%90%86%e3%82%a2%e3%83%89%e3%83%ac%e3%82%b9%e5%a4%89%e6%8f%9b%e3%81%ae%e3%81%be%e3%81%a8%e3%82%81/
Sv32において、仮想アドレスのページテーブルは以下のように定義されています。
VPNと定義されるVirtalPageNumberがVPN[1]とVPN[0]の2つ定義されています。これはRISCVで定義されているページング機構が2段ページテーブルであるためです。
その他下位12bitでオフセットが定義されています。
また、satpレジスタというアドレス変換で使用するためのCSRレジスタがあるのでそちらも使用していきます。
MODEではページング機構に何を使用するか設定します。ドキュメント上、Sv32は1です。 ASIDの使用は必須ではないので今回は実装に使用しません。
Sv32のページテーブルに基づいて、ページテーブルを作成する関数は以下です。
#define SATP_SV32 (1u << 31) #define PAGE_V (1 << 0) // 有効化ビット #define PAGE_R (1 << 1) // 読み込み可能 #define PAGE_W (1 << 2) // 書き込み可能 #define PAGE_X (1 << 3) // 実行可能 #define PAGE_U (1 << 4) // ユーザーモードでアクセス可能 void map_page(uint32_t *table1, uint32_t vaddr, paddr_t paddr, uint32_t flags) { if (!is_aligned(vaddr, PAGE_SIZE)) PANIC("unaligned vaddr %x", vaddr); if (!is_aligned(paddr, PAGE_SIZE)) PANIC("unaligned paddr %x", paddr); uint32_t vpn1 = (vaddr >> 22) & 0x3ff; if ((table1[vpn1] & PAGE_V) == 0) { // 2段目のページテーブルが存在しないので作成する uint32_t pt_paddr = alloc_pages(1); table1[vpn1] = ((pt_paddr / PAGE_SIZE) << 10) | PAGE_V; } // 2段目のページテーブルにエントリを追加する uint32_t vpn0 = (vaddr >> 12) & 0x3ff; uint32_t *table0 = (uint32_t *) ((table1[vpn1] >> 10) * PAGE_SIZE); table0[vpn0] = ((paddr / PAGE_SIZE) << 10) | flags | PAGE_V; }
また、アプリケーションではユーザモードのスタックだけでなくカーネルモードのスタックも保持する実装となるのでどちらもページングを実装します。
まずは、リンカスクリプトで__kernel_baseを定義します。
ENTRY(boot) SECTIONS { . = 0x80200000; __kernel_base = .;
また、プロセス管理構造体で1段目のページテーブルを指すポインタを設定します。
struct process { int pid; int state; vaddr_t sp; uint32_t *page_table; uint8_t stack[8192]; };
次に、create_processの時点でカーネルのページングを設定して、kernel_baseからfree_ram_endまですべてをマッピングすることでアクセス可能にさせます。
extern char __kernel_base[]; struct process *create_process(uint32_t pc) { /* 省略 */ uint32_t *page_table = (uint32_t *) alloc_pages(1); // カーネルのページをマッピングする for (paddr_t paddr = (paddr_t) __kernel_base; paddr < (paddr_t) __free_ram_end; paddr += PAGE_SIZE) map_page(page_table, paddr, paddr, PAGE_R | PAGE_W | PAGE_X); proc->pid = i + 1; proc->state = PROC_RUNNABLE; proc->sp = (uint32_t) sp; proc->page_table = page_table; return proc; }
最後にスイッチコンテキスト時にページテーブルを切り替える実装です。
satpにページテーブルを設定しています。
また、sfence.vma命令で「ページテーブルへの変更をきちんと完了させることを保証する」「ページテーブルエントリのキャッシュを消す」ということを行っています。
void yield(void) { /* 省略 */ __asm__ __volatile__( "sfence.vma\n" "csrw satp, %[satp]\n" "sfence.vma\n" "csrw sscratch, %[sscratch]\n" : // 行末のカンマを忘れずに! : [satp] "r" (SATP_SV32 | ((uint32_t) next->page_table / PAGE_SIZE)), [sscratch] "r" ((uint32_t) &next->stack[sizeof(next->stack)]) ); switch_context(&prev->sp, &next->sp); }