上一篇

小源:從零開始寫 OS (5) —— 時鐘中斷?

zhuanlan.zhihu.com圖標

概要

本章我們將介紹一種常見的內存管理方式:分頁。頁表是為了實現分頁而創建的數據結構,直觀來看,其功能就是將虛擬地址轉換為物理地址。本章我們將學習:

  1. 什麼是虛擬地址。
  2. 什麼是頁表。
  3. 頁目錄和頁表。
  4. 通過 bbl 中建立的簡陋的頁表加深對頁表的理解。

虛擬地址

操作系統的一個主要任務是將程序彼此隔離。比如,你在瀏覽網頁時,並不應該干擾你的文本編輯器。而在編寫瀏覽器或文本編輯器的時候,必然會需要對內存進行操作。然而編寫文本編輯器的人也許並不認識編寫瀏覽器的人。所以他們並不知道對方需要使用那部分的內存。

讓我們先來看一個例子,在 C++ 文件中輸入以下代碼:

#include <iostream>
using namespace std;

int main() {
int a = 1;
int *p = &a;
cout << p << endl;
return 0;
}

編譯運行,在我的電腦(64 位)中,輸出的結果是 0x7ffeefbff5c8 ,這裡輸出的地址叫做虛擬地址。在計算機真實訪問內存的時候,並不會直接訪問 0x7ffeefbff5c8 這個物理地址,而是先進行一個轉換。可假想這個轉換有一個函數,叫 virtual2physical(address) ,那麼實際訪問的內存則是: virtual2physical(0x7ffeefbff5c8) 。

虛擬地址可能是不唯一的,但物理地址一定是唯一的。比如,不同的虛擬地址,可能可以轉換成同一個物理地址(這類似一個哈希的過程)。這樣,程序員在編寫程序時,並不需要考慮內存衝突的問題,因為操作系統會給他分配一片「連續的虛擬內存」(即便他們的物理地址不一定連續)。

這段話看不懂沒關係,以後學到了進程線程可能就懂了

頁表

頁表就是一個用於將虛擬地址轉換為物理地址的工具。在 riscv32 中,如果 satp 的第 31 位為 1 ,則表示啟用頁表機制。在訪問虛擬內存時(開啟頁表時只允許訪問虛擬內存),內存管理單元 MMU(Memory Management Unit) 會用過頁表將虛擬地址轉換為物理地址,再進行訪問,

分頁技術的核心思想是將虛擬內存空間和物理內存空間視為固定大小的小塊,虛擬內存空間的塊稱為 頁面(pages) ,物理地址空間的塊稱為 幀(frames) ,每一個頁都可以映射到一個幀上。

riscv32 框架下,每個頁面的大小為 4kb 。虛擬地址可以分為兩部分, VPN(virtual page number)page offset

他們的作用如下:

  • 通過頁表,將 VPN 轉換為目標物理地址所處的頁面
  • 通過 page offset 在頁面中找到具體的物理地址

由於頁面大小為 4kb ,所以為了能夠訪問頁面中的任意物理位置, page offset 的長度為 12 位。

2^12 byte = 4kb

二級頁表

riscv32 框架採用了二級頁表。一個進程只有一個 根頁表 ,不同的進程有不同的 根頁表 ,所以對於不同的進程,相同的虛擬地址可以轉換到不同的物理地址。

根頁表 也稱為 頁目錄 。二級頁表地址的虛實轉換可以簡要的比喻為:

  • 通過頁表,將 VPN 轉換為目標物理地址所處的頁面
  1. 通過 頁目錄VPN[1] 找到所需 頁目錄項
  2. 頁目錄項 包含了 葉結點頁表(簡稱頁表) 的起始地址,通過 頁目錄項 找到 頁表
  3. 通過 頁表VPN[0] 找到所需 頁表項
  4. 頁表項 包含了目標頁面的起始物理地址,通過 頁表項 找到目標頁面。
  • 通過 page offset 在頁面中找到具體的物理地址

頁表中保存的地址均為物理地址

bbl 中的頁表

其實我們的 os 已經通過 bbl 開啟了分頁機制:

// in ../riscv-pk/bbl/bbl.c

static void setup_page_table_sv32()
{
// map kernel 0x80000000 -> 0xC0000000..
int i_end = dtb_output();
for(unsigned int i = 0x80000000; i <= i_end; i += MEGAPAGE_SIZE) {
root_table[(i + 0x40000000) / MEGAPAGE_SIZE] = pte_create(i >> 12, PTE_R | PTE_W | PTE_X);
}
}

這個頁表極其簡陋,後續將重新建立頁表

這裡 bbl 似乎只設置了頁目錄,沒有設置頁表,會出問題嗎?

其實不會,在 x86 中,有一種類型的頁表叫做 大頁 ,並且頁表項中專門有一位用於判斷頁面是否為大頁。而 riscv 中則由別的做法。 riscv32 中, 頁表項/頁目錄項 的結構如下:

  • 如果 X, W, R 位均為 0 ,則表示這是一個頁目錄項,包含了下一級頁表的物理地址。
  • 否則表示這是一個頁表項,包含了頁面的物理地址。

下面給出 XWR 位為不同值時表達的意思:

因此,在 bbl 中, 根頁表 指向的地方是一個頁面,而且是一個大頁。

riscv 中, satp(Supervisor Address Translation and Protection,監管者地址轉換和保護)寄存器的低 22 位保存了 根頁表 的物理地址(類似於 X86 的 CR3 寄存器)。

32 位下根頁表為二級頁表,64 位下根頁表為四級頁表

我們可以通過 satp 寄存器獲得頁目錄的物理地址。但是由於我們開啟了頁表機制,所以並不能通過直接訪問到物理地址。為了能夠直接對頁表進行一些操作,由兩種常見的辦法:自映射和線性映射。bbl 中建立的頁表就是線性映射的。

自映射真是太難了(小聲bb

線性映射頁表中,虛擬地址和物理地址的關係如下:

virtual address = physical address + offset

內核的起始地址為 0x80000000,bbl 中建立的頁表的將其映射到 0xC0000000 ,所以 offset 為 0x40000000 。

基於 bbl 建立的頁表,我們只需要進行一些簡單的設置:

首先創建 memory/mod.rs ,並在 lib.rs 中加入 mod memory

內存的初始化如下:

pub fn init(dtb: usize) {
unsafe {
// Allow user memory access
sstatus::set_sum();
}
}

這裡我們將 sum 位設為 1 。sum(permit supervisor user memory access)位修改 S 模式讀、寫和指令獲取訪問虛擬內存的許可權。僅當 sum = 1 時則允許在 S 模式下訪問屬於 U 模式(U = 1)的內存,否則會產生異常。

這一步現在還不是必須的,要到用戶程序時才能體現出作用

有了頁表,自然會有頁面異常,比如在讀、寫等操作時缺少許可權:

pub enum PageFault{
LoadPageFault,
StorePageFault,
}

use crate::context::TrapFrame;
pub fn do_pgfault(tf: &mut TrapFrame, style: PageFault) {
match style {
PageFault::LoadPageFault => panic!("load pagefault"),
PageFault::StorePageFault => panic!("store pagefault"),
}
}

由於我們目前還無法處理這些異常,所以先簡單的 panic 處理。

init.rs 中加入 use crate::memory::init as memory_init ,修改 rust_main 為:

#[no_mangle]
pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
interrupt_init();
println!("Hello RISCV ! in hartid {}, dtb @ {:#x} ", hartid, dtb);
memory_init(dtb);
clock_init();
loop {}
}

執行 make asm ,下面只展示部分輸出:

c0020000 <_start>:
c0020000: 00150293 addi t0,a0,1
c0020004: 01029293 slli t0,t0,0x10
c0020008: c0026137 lui sp,0xc0026
c002000c: 00010113 mv sp,sp
c0020010: 00510133 add sp,sp,t0
c0020014: 00000097 auipc ra,0x0
c0020018: 008080e7 jalr 8(ra) # c002001c <rust_main>

c002001c <rust_main>:
c002001c: ef010113 addi sp,sp,-272 # c0025ef0 <ebss+0xfff7feec>
c0020020: 10112623 sw ra,268(sp)
c0020024: 10812423 sw s0,264(sp)
c0020028: 11010413 addi s0,sp,272
c002002c: f2b42a23 sw a1,-204(s0)
...

make asm 的作用是通過反彙編顯示程序執行的彙編代碼

顯然,這些存放指令的地址是可讀的。

由於 bbl 中建立的頁表太簡陋了,這部分目前甚至是可寫的。。。

我們隨便選擇一條指令存放的地址,在 loop{} 前加上:let x_ptr: *mut u32 = 0xc0020020 as *mut u32

執行 make run ,終端輸出 0x10112623 。發現,這與我們通過反彙編得到的指令是一致的。這說明我們成功的通過頁表正確的訪問到了內存。

預告

前面多次提到 bbl 中建立的頁表過於簡陋,比如代碼段居然是 可寫 的!而且相信大家看完文字描述的頁表機制,依然不能很好的理解頁表。。。

反正我不能。。。我在沒寫過頁表的時候就把這篇文章寫完了,但是動手實現完頁表之後發現自己之前寫的文章錯漏百出而且讓人看不懂。。。你現在看到的這一篇是我重寫過的。。。

所以接下來,讓我們將理論與實踐相結合,自己動手實現頁表!

其實不能,因為還需要實現內存分配

下一篇

小源:從零開始寫 OS (7) —— 內存分配?

zhuanlan.zhihu.com圖標
推薦閱讀:

相关文章