Skip to content

Latest commit

 

History

History
266 lines (200 loc) · 10.3 KB

虚拟内存管理.md

File metadata and controls

266 lines (200 loc) · 10.3 KB

虛擬內存管理

實現分頁機制

在 AArch64 體系結構中,將內存地址分為物理地址和虛擬地址,物理地址是實際訪問內存的地址,而虛擬地址是程序指令中使用的地址。虛擬地址通過 MMU 來轉換成物理地址。

內核地址空間和用戶地址空間的隔離

因為 Rustable 需要支持一定數量的用戶進程的并發,所以每個進程的頁表需要經常切換,但內核的頁表甚少切換,所以 Armv8 提供了 TTBR0_EL1 和 TTBR1_EL1 兩個頁表地址來給我們實現 內核地址空間和用戶地址空間的隔離。如果頁表是 TTBR0 ,則表示是用戶地址,其虛擬地址的高位全是 0 。如果頁表為 TTBR1 ,則表示是內核地址,其虛擬地址的高位全是 1 。

我們需要把上述兩塊地址空間映射到 40 位的物理地址。

[圖]

虛擬地址到物理地址的轉換

因為我們使用 4KB 為物理頁的大小,所以硬件要使用 4 級頁表。虛擬地址中有 48 位需要用來轉換,每級頁表使用 9 位,所以每級頁表有 512 項。最後 12 位用來選擇一個 4KB 頁中的地址。

VA bits [47:39] VA bits [38:30] VA bits [29:21] VA bits [20:12] VA bits [11:0]
第 0 級頁表 第 1 級頁表 第 2 級頁表 第 3 級頁表 頁的 offset
指向第一級頁表 指向第二級頁表,1GB block 的基地址 指向第三級頁表,2MB block 的基地址 每個頁表項是一個 4KB block 的基地址 即 PA[11:0]

開啟 MMU

树莓派启动后,kernel8.img 中的二进制代码会被加载到物理内存 0x80000 位置上。此时,MMU 处于关闭状态,访存指令访问的地址即为内存的物理地址。为了将内核与用户程序在内存中隔离开来,我们需要将内核加载至高地址空间( 0xffffff0000800000 ),并确保用户程序无权读写内存中的内核部分。

kernel8.img 显然无法被直接加载至物理地址 0xFFFFFF0000080000,因为物理内存远没有这么大;所以我們通过在 bootloader 中设置页表並打開 MMU,建立起高地址空间到低地址空间的线性映射。然後從串口中接受 kernel 並把它寫到 0xFFFFFF0000080000 上。

於是我們在 bootloader 的 init.S 中,在切換完特權級 EL1 後,加入以下代碼,跳到一段負責填寫頁表的 Rust 代碼。並使能指能的 cache。

vm:
    bl vm_init

    mrs x0, sctlr_el1
    orr x0, x0, #(1 << 12)
    msr sctlr_el1, x0        // enable instruction cache

我們把頁表放在 0x1000000 的物理地址,在 vm_init 中,首先分配一個頁給頁表,然後使用 boot_map_segment 函數來建立起高地址和低地址的線性映射,並填寫頁表。 boot_map_segment 函數的實現基本與 ucore 中的一致。

pub extern "C" fn vm_init() {
    let mut binary_end = 0x1000000;
    
    unsafe { FREEMEM = align_up(binary_end as usize, PGSIZE); }

    let mut pgdir = boot_alloc(PGSIZE, true).expect("out of memory");
    
    let n = align_up(MAXPA, PGSIZE);
    boot_map_segment(pgdir, 0, n, 0, ATTRINDX_NORMAL);
    boot_map_segment(pgdir, n, n, n, ATTRINDX_DEVICE);
}

填寫完頁表後回到 bootloader,開始使能 MMU。

首先設置 TTBR0 和 TTBR1 頁表的地址,即把 TTBR0_EL1TTBR1_EL1 寄存器設為 0x1000000

el1_mmu_activate:
    ldr x0, =0x04cc
    msr mair_el1, x0
    isb
    
    // Translation table base address
    ldr x1, =0x01000000
    msr ttbr0_el1, x1
    msr ttbr1_el1, x1
    isb

初始化 TCR_EL1 寄存器的值,即 Translation Control Register, EL1:

    mrs x2, tcr_el1
    ldr x3, =0x70040ffbf
    bic x2, x2, x3

TCR_EL1 寄存器的各個位設置如下

  • bits [34:32] = 010: Intermediate Physical Address Size. 40-bit, 1TByte.
  • bits [31] = 1: Reseverd
  • bits [30] = 0: TTBR1_EL1 的粒度為 4KB
  • bits [29:28] = 11: Shareability attribute for memory associated with translation table walks using TTBR1. Inner Shareable.
  • bits [27:26] = 11: Outer cacheability attribute for memory associated with translation table walks using TTBR1. Normal memory, Outer Write-Back no Write-Allocate Cacheable.
  • bits [25:24] = 11: Inner cacheability attribute for memory associated with translation table walks using TTBR1. Normal memory, Inner Write-Back no Write-Allocate Cacheable.
  • bits [21:16] = 011000: bSize offset of the memory region addressed by TTBR1
  • bits [13:12] = 11: Shareability attribute for memory associated with translation table walks using TTBR0. Inner Shareable.
  • bits [11:10] = 11: Outer cacheability attribute for memory associated with translation table walks using TTBR0. Normal memory, Outer Write-Back no Write-Allocate Cacheable.
  • bits [9:8] = 11: Inner cacheability attribute for memory associated with translation table walks using TTBR0. Normal memory, Inner Write-Back no Write-Allocate Cacheable.
  • bits [5:0] = 011000: Size offset of the memory region addressed by TTBR0.
    ldr x3, =0x2bf183f18
    orr x2, x2, x3
    msr tcr_el1, x2
    isb

設置 SCTLR_EL1 寄存器,即 System Control Register, EL1

Write permission implies Execute Never (XN). You can use this bit to require all memory regions with write permissions are treated as XN. The WXN bit is permitted to be cached in a TLB.

    ldr x5, =kmain
    mrs x3, sctlr_el1
    ldr x4, =0x80000
    bic x3, x3, x4

SCTLR_EL1 寄存器的各位設置如下:

  • bits [12] : Instruction caches enabled.
  • bits [2] : Data and unified caches enabled.
  • bits [0] : EL1 and EL0 stage 1 MMU enabled.
    ldr x4, =0x1005
    orr x3, x3, x4
    msr sctlr_el1, x3
    isb
    br x5

至此,MMU 已開始,可以跳到 kernel 執行

go_kmain:
    bl      kmain
    b       1b

頁表管理

由虛擬地址計算物理地址

根據上述的轉換格式就可以實現 get_pte 函數,給定頁表地址和虛擬地址,可以獲取對應的物理地址。

pub fn get_pte(pgdir_addr: *const usize, va: usize, create: bool) -> Result<*mut usize, AllocErr> {
	// 第0級 => 第1級
    let pgtable0_entry_ptr = pgdir_addr as *mut usize;
    let mut pgtable1 = PTE_ADDR(unsafe { *pgtable0_entry_ptr }) + PT1X(va) * 8;
    if (unsafe { *pgtable0_entry_ptr } & PTE_V) == 0 && create == true {
        pgtable1 = alloc_page().expect("cannot alloc page") as usize;
        unsafe { *pgtable0_entry_ptr = pgtable1 | PTE_V };
        pgtable1 += PT1X(va) * 8;
    }
	// 第1級 => 第2級
    let pgtable1_entry_ptr = pgtable1 as *mut usize;
    同上
	// 第2級 => 第3級
    同上

    Ok(pgtable3 as *mut usize)
}
插入頁表

page_insert 函數負責建立給定物理頁 page 和虛擬地址 va 的映射,並插入到給定的頁表 pgdir 中。

此函數首先嘗試通過 get_pte 獲得 va 對應的頁表項 pte 和對應的頁,

  • 若該頁等於 page 則不變
  • 若不等於,則調用 page_remove 把此頁移除

最後把頁表項 pte 設為 page ,並設置權限。因為頁表有所修改,需要更新 TLB。

pub fn page_insert(pgdir: *const usize, page: *mut Page, va: usize, perm: usize) -> Result<i32, i32>{
    let PERM = perm | PTE_V | ATTRINDX_NORMAL | ATTRIB_SH_INNER_SHAREABLE | AF;
    match get_pte(pgdir, va, true) {
        Ok(pte) => {
            (unsafe { &mut *page }).page_ref_inc();
            if unsafe{ *pte & PTE_V != 0} {
                if pa2page(PTE_ADDR(unsafe{*pte})) != page {
                    page_remove(pte);
                } else {
                    (unsafe { &mut *page }).page_ref_dec();
                }
            }
            unsafe{ *pte = PTE_ADDR(page2pa(page)) | PERM };
            tlb_invalidate();
            return Ok(0);
        },
        Err(_) => {
            return Err(-1);
        }
    }
}
從頁表中移除頁

page_remove 函數是把 pte 對應的頁釋放掉。

根據頁表項計算出對應的頁,若此頁的 reference 減一後為 0 ,則把此頁釋放掉。

pub fn page_remove(pte: *mut usize) {
    let pa = unsafe{ PTE_ADDR(*pte as usize) as *mut usize };
    let page = pa2page(pa as usize);
    if (unsafe { &mut *page }).page_ref_dec() <= 0 {
        dealloc_page(pa as *mut u8);
    }
    unsafe { *pte = 0; }
    tlb_invalidate();
}
分配頁並插入頁表

pgdir_alloc_page 函數利用 alloc_pagepage_insert 實現,分配頁並插入到給定頁表。

user_pgdir_alloc_page 函數主要利用 alloc_page_at 函數在用戶的 allocator 中分配虛擬頁,然後把虛擬頁地址利用 pgdir_alloc_page 為其分配物理頁,並插入到用戶的頁表中。

pub fn pgdir_alloc_page(pgdir: *const usize, va: usize, perm: usize) -> Result<*mut u8, AllocErr>

pub fn user_pgdir_alloc_page(allocator: &mut Allocator, pgdir: *const usize, va: usize, perm: usize) -> Result<*mut u8, AllocErr> {
	//分配虛擬頁
    alloc_page_at(allocator, va, pgdir).expect("alloc virtual page failed");
    
    pgdir_alloc_page(pgdir, va, perm)
}
Allocator 的 alloc_at 函數

此函數被用於上述的 user_pgdir_alloc_page 函數

pub fn alloc_at(&mut self, va: usize, layout: Layout, pgdir: *const usize) -> Result<*mut u8, AllocErr> {
    let npage = align_up(layout.size(), PGSIZE) / PGSIZE;
    if npage as u32 > self.n_free {
        return Err( AllocErr::Exhausted { request: layout } );
    }
    
    for i in self.free_list.iter_mut() {
        找到包含va的空閒頁
    }
    match page {
        Some(page) => {
            let prev_npage = 空閒內存塊中前面剩的頁數
            let next_npage = 空閒內存塊中後面剩的頁數

            if next_npage > 0 { 把後面剩的空閒頁插回鏈表 }
            if prev_npage > 0 { 原空閒頁的property設為prev_npage; } else { 刪除原空閒頁 }
            
            把分配的頁設為Used
            self.n_free -= npage as u32;
            return Ok(self.page2addr(alloc_page) as *mut usize as * mut u8);
        }
        _ => { 
            switch_back();
            Err( AllocErr::Exhausted { request: layout } )
        }
    }

}