ここで、 DRAM_BASE が 0x4_0000_0000_0000 であると仮定して、どのようにアドレス変換が行われるかを考えてみよう。
仮想アドレス 0x12345 がどのように変換されるかを考える。
アドレスと、VPNへのマッピングは図のようになる
- VPN[2] = 0x0
- VPN[1] = 0x0
- VPN[0] = 0x12
- Page Offset = 0x678

仮想アドレスの変換は、上位のVPNから順番に変換していく仕組みだ。SATPの指すl1ptをベースとしてVPN[2]=0x0としてl1pt[0]アクセスされる。
l1pt[0]はuser_l2ptへのポインタを指しており、VPN[1]を使った変換に移っていく。
VPN[1]=0x0なので、 user_l2pt[0x0] にアクセスされる。
user_l2pt[0x0]は user_llptへのリンクだ。
しかし、user_llpt[0x12] はPTEを特に何も設定してないのでここでページ・テーブル例外が発生する。
handle_fault()が呼ばれるのだが、引数causeで、addrには元の仮想アドレスが挿入される。
void handle_fault(uintptr_t addr, uintptr_t cause) { assert(addr >= PGSIZE && addr < MAX_TEST_PAGES * PGSIZE); addr = addr/PGSIZE*PGSIZE; if (user_llpt[addr/PGSIZE]) { if (!(user_llpt[addr/PGSIZE] & PTE_A)) { user_llpt[addr/PGSIZE] |= PTE_A; } else { assert(!(user_llpt[addr/PGSIZE] & PTE_D) && cause == CAUSE_STORE_PAGE_FAULT); user_llpt[addr/PGSIZE] |= PTE_D; } flush_page(addr); return; } freelist_t* node = freelist_head; assert(node); freelist_head = node->next; if (freelist_head == freelist_tail) freelist_tail = 0; uintptr_t new_pte = (node->addr >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V | PTE_U | PTE_R | PTE_W | PTE_X; user_llpt[addr/PGSIZE] = new_pte | PTE_A | PTE_D; flush_page(addr); assert(user_mapping[addr/PGSIZE].addr == 0); user_mapping[addr/PGSIZE] = *node; uintptr_t sstatus = set_csr(sstatus, SSTATUS_SUM); memcpy((void*)addr, uva2kva(addr), PGSIZE); write_csr(sstatus, sstatus); user_llpt[addr/PGSIZE] = new_pte; flush_page(addr); asm volatile ("fence.i"); }
具体的にはこうだろう。
1. まずページオフセットの部分を切り落とす。
assert(addr >= PGSIZE && addr < MAX_TEST_PAGES * PGSIZE);
addr = addr/PGSIZE*PGSIZE;
2. user_llpt[addr/PGSIZE]はuser_llptにページが存在しているかをチェックし、
- 存在していた場合
- PTE_Aが設定されていなければ設定する。(AはAccessed Bitを意味する)
- そうでなければ、PTE_Dを設定する (DはDirtyを意味する)
- (ちなみに、"A"はそのページにアクセスされた(読取/書込/実行)ことがある、Dはそのページに対して書き込みが発生したことがある、の意味)
- そのままページをフラッシュしてリターン
そこから先はページが存在しない場合の処理になる。
3. freelist_headから新しいページのフリーリストを取得する。フリーリストのポインタを更新する。
freelist_t* node = freelist_head; assert(node); freelist_head = node->next; if (freelist_head == freelist_tail) freelist_tail = 0;
4. 新たなPTEを作成する。node->addrが新たなページのアドレスとなり、PTEのV/U/R/W/X/A/Dが設定され、user_llptの当該場所に設定される。
uintptr_t new_pte = (node->addr >> PGSHIFT << PTE_PPN_SHIFT) | PTE_V | PTE_U | PTE_R | PTE_W | PTE_X; user_llpt[addr/PGSIZE] = new_pte | PTE_A | PTE_D; flush_page(addr);
5. user_mapping に新たな作成したページをマッピングしておく。これはページのEvictionの時に使うらしい。
assert(user_mapping[addr/PGSIZE].addr == 0); user_mapping[addr/PGSIZE] = *node;
6. sstatus.SUMを設定して、スーパーバイザモードからユーザモードへのページのアクセスを許可する。
- ちなみに、このページ処理はMEDELEGによりユーザモードで処理されるようになっている。
7. さらに、memcpyでもって、addrからuva2kva(addr)へのデータ転送を行う。
uintptr_t sstatus = set_csr(sstatus, SSTATUS_SUM); memcpy((void*)addr, uva2kva(addr), PGSIZE); write_csr(sstatus, sstatus);
uva2kva(addr)の実装はこうだ。マクロの定義ではpaとなっているが、これは仮想アドレスのハズである。
MEGAPAGE_SIZEの定義はMEGAPAGE_SIZE = (PTES_PER_PT * PGSIZE) = ((1UL << RISCV_PGLEVEL_BITS) * PGSIZE) = ((1 << 9) * (1 << 12)) = 512 * 4096 = 0x20_0000 となる。これの実装はちょっと良く分からない。
#define uva2kva(pa) ((void*)(pa) - MEGAPAGE_SIZE)
8. 最後に、user_llpt[addr/PGSIZE] にPTEを設定して終わりだ。
user_llpt[addr/PGSIZE] = new_pte;
flush_page(addr);
flush_page(addr) の実装は以下だ。これは要するにsfence.vmaを実行する。
#define flush_page(addr) asm volatile ("sfence.vma %0" : : "r" (addr) : "memory")