1. 程式人生 > 其它 >6.s081 : 頁表

6.s081 : 頁表

CH3 Page tables

os通過頁表來給每個程序提供私有的地址空間和記憶體. 頁表決定了什麼是記憶體地址以及實體記憶體的什麼部分可以被獲取. 它允許將多個程序地址空間存放在同一個實體記憶體中. 同時也允許將相同記憶體對映到幾個不同的地址空間(trampoline頁).

Paging hardware

RISC-V指令(user和kernel)都操作虛擬地址, 而記憶體是由實體地址索引的. RISC-V頁表在這兩種地址之間建立關聯(通過將虛擬地址對映到實體地址).

xv6只使用39 bits的虛擬地址, 還有25 bits未使用. 一個頁表有2^27個頁表條目(PTE, page table entry). 每個PTE都儲存著44-bit的物理頁號(PPN, physical page number)和一些標誌位.

頁表硬體將虛擬地址的前27(27/39)位用作索引頁表, 後12位與通過前27位索引到的PTE中的PPN結合為56位的實體地址(一個頁的大小為2^12=4096 bytes).

但是真正的翻譯是分三步, 第一層頁表的實體地址存放在satp暫存器中, 第一層L2通過9 bits索引512個PTE, 每個PTE中的PPN儲存著下一層頁表的實體地址, 下一層也是一樣, 知道最後一層PTE中存放的PPN是要翻譯的最終實體地址.

如果查詢序列中3個PTE任意一個PPN不存在, 那麼會page-fault exception. 由於通常大範圍的PTE都為空, 三層查詢可以大大節省實體記憶體(如果第一層只有一個PTE, 那麼剩餘的511個PTE中的PPN為空, 那麼就不需要為其分配下一層頁表, 這樣就只需要2^16 * 2^16 + 2^6).

每個PTE有10位標誌位, 用來告訴翻譯硬體虛擬地址如何被允許使用.

  • PTE_V: 表明是否PTE存在. 如果為0, 對該頁的引用會造成異常.
  • PTE_R: 指令是否允許讀該頁.
  • PTE_W: 指令時候允許寫該頁.
  • PTE_X: 是否cpu可以執行的指令.
  • PTE_U: 是否在user模式下的指令可以獲取頁. 如果PTE_U未被設定, 那麼PTE只能在supervisor模式下執行.

核心通過將根頁表頁的實體地址寫入satp暫存器來使用一個頁表. 每個cpu都有自己的satp, 所以不同的cpu可以執行不同程序.

實體記憶體指的是DRAM中的儲存單元, 每個位元組的實體記憶體分配了一個地址. 指令只使用虛擬地址, 通過翻譯硬體翻譯為實體地址併發送到DRAM硬體來讀寫.

Kernel address space

每個程序都有一個頁表來描述程序的使用者地址空間, 以及只有一個頁表來描述核心地址空間.

實體記憶體從0x80000000開始, 並至少到0x86400000結束(PHYSTOP). 在0x80000000以下的實體地址對映著裝置, qemu通過控制暫存器來控制這些裝置. 核心可以通過讀寫這些特殊的實體地址來和裝置互動, 這種讀寫是直接和裝置硬體交流的而不是實體記憶體.

核心是直接對映的, 也就是說, 虛擬地址與實體地址是一樣的(核心儲存在虛擬地址和實體地址的KERNBASE=0x80000000處). 直接對映簡化了核心讀寫實體記憶體的程式碼.

但有些核心虛擬地址不是直接對映:

  • trampoline頁: 存放在虛擬地址空間的頂部(核心和使用者有相同的結構). trampoline的物理頁在核心虛擬地址空間對映兩次, 一次直接對映, 一次在虛擬地址頂部.
  • 核心的棧頁: 每個程序有它自己的核心棧, 對映在核心虛擬地址的頂部, 所以核心棧之間可以通過未對映的guard page來隔離(PTE是無效的, 當越界時, 會異常).

核心的trampoline和text頁都是PTE_R和PTE_X.

Code: creating an address space

大多數操作地址空間和頁表的程式碼都在vm.c(kernel/vm.c). pagetable_t(要麼是核心頁表, 要麼是每個程序頁表)是指向RISC-V根頁表頁的指標.

幾個重要的函式:

  • walk : 通過pagetable, 找到va對應的實體地址, 如果alloc為1, 那麼為va對映對應的實體地址.
  • mappages: 在pagetable上對映va<->pa(新的PTE). 對於每個虛擬地址, mappages一頁頁得翻譯成實體地址. 先呼叫walk來找到可用的PTE, 再初始化PTE來儲存相關的物理頁號(PPN).
  • copyin, copyout: 從user虛擬地址複製資料/複製資料到user虛擬地址.
  • kvminit: 在boot階段, main呼叫kvminit來建立核心頁表. 由於是在xv6可以翻譯頁表之前, 對映的頁表是直接對映. kvminit首先分配一個實體記憶體頁來儲存根頁表頁. 之後使用kvmmap來翻譯核心需要的實體記憶體(核心指令和資料).
  • Kvminithart: 裝載核心頁表. 通過將根頁表頁實體地址寫入satp暫存器, 之後 cpu將使用核心頁表來翻譯地址.
  • procinit: 通過main呼叫, 為每個程序分配一個核心棧. 通過KSTACK來產生虛擬地址, 將每個棧對映到該虛擬地址. 再通過kvmmap將PTE對映到核心頁表. 並呼叫kvminithart來重新載入核心頁表進satp.
  • sfence.vma: cpu會緩衝頁表條目進TLB(translation look-aside buffer), 所以當xv6改變頁表時, 必須通過sfence.vma來清除快取.

實體記憶體分配

xv6在執行時, 會分配和釋放從核心頂部到PHYSTOP(0x86400000)的實體記憶體. 一次分配和釋放一頁(4096bytes).

實體記憶體分配器

分配器在kalloc.c中, 分配器是一個可分配實體記憶體頁的空閒連結串列.

main呼叫kinit函式來初始化分配器. kinit通過初始化空閒連結串列來儲存從核心頂部到PHYSTOP的記憶體(128MB).

kfree通過將要釋放的記憶體中的每一個位元組設定為1, 這會導致之後使用這段空閒記憶體的程式碼只能看到垃圾資料從而更快崩潰. 之後將pa轉化為run指標, 並將其儲存在空閒連結串列中.

程序地址空間

當一個程序需要記憶體時, xv6先使用kalloc來分配實體記憶體, 之後將指向新分配的實體記憶體的PTE加入到該程序的頁表中, 並設定標誌位.

幾個頁表特性:

  • 不同程序的頁表將user地址翻譯到不同的實體記憶體.
  • 每個程序都認為它的記憶體是連續的, 從0開始的.
  • 核心將一個有trampoline程式碼的頁對映到user地址空間的頂部, 所以trampoline這個頁出現在所有的虛擬地址空間.

棧佔一個頁. 先是儲存著args引數和指向args引數的指標, 接下來是允許程式從main開始執行的值. 為了檢測棧是否溢位, 在棧下放置了一個guard頁, 有效位設定為0.