Mit6.S081筆記Lab3: page tables 頁表

Amroning發表於2024-10-26

課程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.S081/2020/labs/pgtbl.html
我的程式碼地址:https://github.com/Amroning/MIT6.S081/tree/pgtbl
相關翻譯:http://xv6.dgs.zone/labs/requirements/lab3.html
參考部落格:https://www.cnblogs.com/weijunji/p/14338430.html
https://blog.miigon.net/posts/s081-lab3-page-tables/

Lab3: page tables

實驗3實驗較難理解。該實驗主要學習xv6的⻚表機制,提⾼⽤戶空間和核心空間之間傳遞資料的效率,重點閱讀原始碼kernel/vm.c

定義一個名為vmprint()的函式。它應當接收一個pagetable_t作為引數,並以下面描述的格式列印該頁表。在exec.c中的return argc之前插入if(p->pid==1) vmprint(p->pagetable),以列印第一個程序的頁表。如果你透過了pte printout測試的make grade,你將獲得此作業的滿分。

現在,當您啟動xv6時,它應該像這樣列印輸出來描述第一個程序剛剛完成exec()inginit時的頁表:

page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

第一行顯示vmprint的引數。之後的每行對應一個PTE,包含樹中指向頁表頁的PTE。每個PTE行都有一些“..”的縮排表明它在樹中的深度。每個PTE行顯示其在頁表頁中的PTE索引、PTE位元位以及從PTE提取的實體地址。不要列印無效的PTE。在上面的示例中,頂級頁表頁具有條目0和255的對映。條目0的下一級只對映了索引0,該索引0的下一級對映了條目0、1和2。

您的程式碼可能會發出與上面顯示的不同的實體地址。條目數和虛擬地址應相同。

該實驗需要實現一個列印頁表內容的函式,以示例所示的格式列印傳進的頁表。

​ 在Sv39模式下,頁表是一個三級樹型結構根頁表是這棵樹的根節點,它是一個4KB(4096位元組)的頁,每個頁有512個PTE,每個PTE記錄了下一級頁表的位置(也就是下一級頁表的實體地址,最後一級頁表的PTE指向的是最終對映的實體地址)。

​ 需要模擬查詢頁表的過程,對三級頁表進行遍歷並列印。而kernel/vm.c中的freewalk()函式已經實現了遞迴遍歷頁表並將其釋放,所以只要模仿其邏輯實現列印功能即可。程式碼:

// kernel/vm.c
//遞迴列印頁表
int pgtblprint(pagetable_t pagetable, int depth) {
    // there are 2^9 = 512 PTEs in a page table.
    for (int i = 0; i < 512; i++) {
        pte_t pte = pagetable[i];

        if (pte & PTE_V) {      //如果頁表項有效,按格式列印頁表項
            printf("..");
            for (int j = 0;j < depth;++j)
                printf(" ..");
            printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));


            //如果該節點不是葉節點,遞迴列印子節點
            if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
                // this PTE points to a lower-level page table.
                uint64 child = PTE2PA(pte);
                pgtblprint((pagetable_t)child, depth + 1);
            }
        }
    }

    return 0;
}

//列印頁表
int vmprint(pagetable_t pagetable) {
    printf("page table %p\n", pagetable);
    return pgtblprint(pagetable, 0);
}

然後在核心標頭檔案新增函式宣告:

// kernel/defs.h
// vm.c
......
int             copyout(pagetable_t, uint64, char *, uint64);
int             copyin(pagetable_t, char *, uint64, uint64);
int             copyinstr(pagetable_t, char*, uint64, uint64);
int             vmprint(pagetable_t pagetable);         //列印頁表內容函式宣告

按照實驗需求,在exec.c中的return argc之前插入if(p->pid==1) vmprint(p->pagetable),以列印第一個程序的頁表:

int
exec(char *path, char **argv)
{
......
  if (p->pid == 1)
      vmprint(p->pagetable);        //exec返回之前列印一下頁表

  return argc; // this ends up in a0, the first argument to main(argc, argv)

 bad:
  if(pagetable)
    proc_freepagetable(pagetable, sz);
  if(ip){
    iunlockput(ip);
    end_op();
  }
  return -1;
}

到此可以執行make qemu啟動xv6,驗證是否得到和示例相似的結果,列印出了第一個程序的頁表

A kernel page table per process (hard)

你的第一項工作是修改核心來讓每一個程序在核心中執行時使用它自己的核心頁表的副本。修改struct proc來為每一個程序維護一個核心頁表,修改排程程式使得切換程序時也切換核心頁表。對於這個步驟,每個程序的核心頁表都應當與現有的的全域性核心頁表完全一致。如果你的usertests程式正確執行了,那麼你就透過了這個實驗。

​ 當前xv6作業系統中,在使用者態下的每個使用者程序都使用各自的使用者態頁表。一旦進入了核心態(例如系統呼叫)就會切換到核心態頁表(透過修改 satp 暫存器,trampoline.S)。然而這個核心態頁表是全域性共享的,所有程序進入核心態之後都會共用一個核心態頁表。

​ 共享一個核心頁表有什麼弊端呢?

​ 程序可能會意外或惡意地訪問其他程序的核心資料。如果一個程序因為 bug 或惡意操作訪問了核心中的敏感資料,它可能會影響其他程序或系統的整體穩定性。

​ 每次建立或刪除程序時,都需要小心更新共享的頁表條目,以確保不同程序之間的記憶體不會衝突或被錯誤覆蓋。這會增加系統的複雜性,並且在多核系統中,這種全域性共享的管理會增加同步開銷和衝突的可能性

​ 如果每個程序進入核心態之後,都能有自己獨立的核心頁表,可以避免很多麻煩,這就是這個實驗的目的。

現在程序的結構體proc中新增一個新的核心頁表屬性,用來儲存程序獨享的核心態頁表:

// kernel/proc.h
// Per-process state
struct proc {
......
  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
  pagetable_t kernelpgtbl;     //儲存程序獨享的核心態頁表
};

核心程序需要依賴核心頁表內一些固定的對映才能正常工作,例如 UART 控制、硬碟介面、中斷控制等。而 kvminit 原本只為全域性核心頁表 kernel_pagetable 新增這些對映,所以接下來大幅度改動kernel/vm.c,使其他程序也可以建立獨享的核心頁表。

先將原本的kvminit抽象,全域性核心頁表仍然使用這個函式來初始化:

/*
 * create a direct-map page table for the kernel.
 */
void
kvminit()
{
    kernel_pagetable = kvminit_newpgtbl(); // 仍然需要有全域性的核心頁表,用於核心 boot 過程,以及無程序在執行時使用。
}

實現kvminit_newpgtbl函式,建立一個頁表並初始化對映,返回這個頁表:

pagetable_t
kvminit_newpgtbl()
{
    pagetable_t pgtbl = (pagetable_t) kalloc();
    memset(pgtbl, 0, PGSIZE);

    kvm_map_pagetable(pgtbl);

    return pgtbl;
}

初始化頁表對映函式kvm_map_pagetable

void kvm_map_pagetable(pagetable_t pgtbl) {
    // 將各種核心需要的 direct mapping 新增到頁表 pgtbl 中
    
    // uart registers
    kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

    // virtio mmio disk interface
    kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

    // CLINT
    kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

    // PLIC
    kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

    // map kernel text executable and read-only.
    kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

    // map kernel data and the physical RAM we'll make use of.
    kvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

    // map the trampoline for trap entry/exit to
    // the highest virtual address in the kernel.
    kvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

現在普通程序也可以透過呼叫kvminit_newpgtbl函式來建立自己的核心頁表了,此時在核心態中就有兩種頁表:一種是核心程序獨享的頁表,另一種是其他程序各自獨享的頁表。所以關於核心頁表處理的一些函式需要做一些改動。

比如kvmmap函式,將虛擬地址對映到實體地址,現在只處理核心程序的頁表,所以修改一下讓這個函式可以處理所有的頁表:

void
kvmmap(pagetable_t pgtbl, uint64 va, uint64 pa, uint64 sz, int perm)            // 將某個邏輯地址對映到某個實體地址(新增第一個引數 pgtbl)
{
    if(mappages(pgtbl, va, sz, pa, perm) != 0)
        panic("kvmmap");
}

還有一個kvmpa函式,將虛擬地址翻譯成實體地址,原來也是隻處理核心程序的頁表,同樣修改一下:

uint64
kvmpa(pagetable_t pgtbl, uint64 va)         // kvmpa 將核心邏輯地址轉換為實體地址(新增第一個引數 kernelpgtbl)
{
    uint64 off = va % PGSIZE;
    pte_t *pte;
    uint64 pa;

    pte = walk(pgtbl, va, 0);			//kernel_pagetable改為引數pgtbl
    if (pte == 0)
        panic("kvmpa");
    if ((*pte & PTE_V) == 0)
        panic("kvmpa");
    pa = PTE2PA(*pte);
    return pa + off;
}

這樣可以建立程序間相互獨立的核心頁表了,但是還有一個東西需要處理:核心棧。 原本的 xv6 設計中,所有處於核心態的程序都共享同一個頁表,即意味著共享同一個地址空間。由於 xv6 支援多核/多程序排程,同一時間可能會有多個程序處於核心態,所以需要對所有處於核心態的程序建立其獨立的核心態內的棧,也就是核心棧,供給其核心態程式碼執行過程。

在已經新增的新修改中,每一個程序都會有自己獨立的核心頁表。而現在需要每個程序只訪問自己的核心棧,所以可以把每個程序的核心棧對映到各自核心頁表的固定位置(不同頁表內的同一邏輯地址,指向不同實體記憶體)

原本xv6為每一個程序分配好核心棧(在共享空間中),所以先把這部分程式碼去掉:

// kernel/proc.c
// initialize the proc table at boot time.
void
procinit(void)
{
    struct proc *p;
    
    initlock(&pid_lock, "nextpid");
    for(p = proc; p < &proc[NPROC]; p++) {
        initlock(&p->lock, "proc");

        // Allocate a page for the process's kernel stack.
        // Map it high in memory, followed by an invalid
        // guard page.
        //   char *pa = kalloc();
        //   if(pa == 0)
        //     panic("kalloc");
        //   uint64 va = KSTACK((int) (p - proc));
        //   kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
        //   p->kstack = va;

        //註釋掉了上面的程式碼(為所有程序預分配核心棧的程式碼),變為建立程序的時候再建立核心棧,見 allocproc()
    }
    kvminithart();
}

在建立程序的時候,為程序建立獨立的核心頁表,然後將專屬的核心棧固定到核心頁表的固定位置,建立對映:

static struct proc*
allocproc(void)
{
    ......
        
    // An empty user page table.
    p->pagetable = proc_pagetable(p);
    if(p->pagetable == 0){
        freeproc(p);
        release(&p->lock);
        return 0;
    }

    // 為新程序建立獨立的核心頁表,並將核心所需要的各種對映新增到新頁表上
    p->kernelpgtbl = kvminit_newpgtbl();

    // 分配一個物理頁,作為新程序的核心棧使用
    char* pa = kalloc();
    if (pa == 0)
        panic("kallo");
    uint64 va = KSTACK((int)0);     // 將核心棧對映到固定的邏輯地址上
    kvmmap(p->kernelpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
    p->kstack = va;     // 記錄核心棧的邏輯地址,其實已經是固定的了,依然這樣記錄是為了避免需要修改其他部分 xv6 程式碼

    // Set up new context to start executing at forkret,
    // which returns to user space.
    memset(&p->context, 0, sizeof(p->context));
    p->context.ra = (uint64)forkret;
    p->context.sp = p->kstack + PGSIZE;

    return p;
}

現在程序的核心頁表就建立完成了,但是程序進入核心態時還是會使用全域性的核心程序頁表,需要在 scheduler() 中進行相關修改。在排程器將 CPU 交給程序執行之前,載入程序的核心頁表到SATP暫存器,切換到該程序對應的核心頁表:

void
scheduler(void)
{
		......
        p->state = RUNNING;
        c->proc = p;

        // 切換到程序獨立的核心頁表
        w_satp(MAKE_SATP(p->kernelpgtbl));
        sfence_vma();       // 清除快表快取,重新整理TLB快取,以確保地址轉換表的更改生效

         // 排程,執行程序
        swtch(&c->context, &p->context);

        // 切換回全域性核心頁表
        kvminithart();

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;
		......
}

現在,每個程序都會在核心態使用自己獨立的核心頁表了

在程序結束後,應該釋放程序獨享的頁表以及核心棧,回收資源,否則會導致記憶體洩漏。

原本釋放記憶體的函式在kernel/proc.c中,在此修改。這裡按建立的順序反著來,先釋放程序的核心棧,再釋放程序的核心頁表:

static void
freeproc(struct proc *p)
{
    if(p->trapframe)
        kfree((void*)p->trapframe);
    p->trapframe = 0;
    if(p->pagetable)
        proc_freepagetable(p->pagetable, p->sz);
    p->pagetable = 0;
    p->sz = 0;
    p->pid = 0;
    p->parent = 0;
    p->name[0] = 0;
    p->chan = 0;
    p->killed = 0;
    p->xstate = 0;

    //釋放程序的核心棧
    void* kstack_pa = (void*)kvmpa(p->kernelpgtbl, p->kstack);
    kfree(kstack_pa);
    p->kstack = 0;

    // 此處不能使用 proc_freepagetable,因為其不僅會釋放頁表本身,還會把頁表內所有的葉節點對應的物理頁也釋放掉。
    // 這會導致核心執行所需要的關鍵物理頁被釋放,導致核心崩潰。
    
    // 遞迴釋放程序獨享的頁表,釋放頁表本身所佔用的空間,但不釋放頁表指向的物理頁
    kvm_free_kernelpgtbl(p->kernelpgtbl);
    p->kernelpgtbl = 0;
    p->state = UNUSED;
}

如果使用proc_freepagetable函式,會同時釋放掉核心程序必要的對映,導致核心崩潰。proc_freepagetable函式如下:

// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmfree(pagetable, sz);
}

所以在vm.c中另寫一個kvm_free_kernelpgtbl函式,不釋放頁表指向的物理頁:

// 遞迴釋放一個核心頁表中的所有 mapping,但是不釋放其指向的物理頁
void
kvm_free_kernelpgtbl(pagetable_t pagetable) {
    for (int i = 0;i < 512;++i) {
        pte_t pte = pagetable[i];
        uint64 child = PTE2PA(pte);
        if ((pte & PTE_V) && (pte & (PTE_R | PTE_W | PTE_X)) == 0) {      // 如果該頁表項指向更低一級的頁表
            kvm_free_kernelpgtbl((pagetable_t)child);                     // 遞迴釋放低一級頁表及其頁表項
            pagetable[i] = 0;
        }
    }
    kfree((void*)pagetable);        // 釋放當前級別頁表所佔用空間
}

最後一個小問題。因為上面改動了kvmpa函式,這個函式在virtio_disk.c中也呼叫了,所以在這裡也做出對應修改:

// virtio_disk.c
#include "proc.h" // 新增標頭檔案引入

 ......

void
virtio_disk_rw(struct buf *b, int write)
{
 ......
disk.desc[idx[0]].addr = (uint64) kvmpa(myproc()->kernelpgtbl, (uint64) &buf0); // 呼叫 myproc(),獲取程序核心頁表
 ......
}

現在可以執行./grade-lab-pgtbl usertests,驗證實驗是否完成

Simplify copyin/copyinstr (hard)

將定義在kernel/vm.c*中的copyin的主題內容替換為對copyin_new的呼叫(在kernel/vmcopyin.c*中定義);對copyinstrcopyinstr_new執行相同的操作。為每個程序的核心頁表新增使用者地址對映,以便copyin_newcopyinstr_new工作。如果usertests正確執行並且所有make grade測試都透過,那麼你就完成了此項作業。

​ 上一個實驗已經讓每一個程序都有獨立的核心態頁表了,該實驗需要將使用者態的對映新增到每個程序的核心頁表,也就是將使用者態的頁表複製到核心態的頁表。這樣使得核心態也可以對使用者態傳進來的指標(邏輯地址)進行解引用。

​ 原來的copyin函式透過軟體模擬訪問頁表的過程獲取實體地址的,而在核心頁表內維護對映副本的話,可以利用 CPU 的硬體定址功能進行定址,效率更高並且可以透過快表加速。

​ 要實現這樣的效果,我們需要在每一處核心對使用者頁表進行修改的時候,將同樣的修改也同步應用在程序的核心頁表上,使得兩個頁表的程式段(0 到 PLIC 段)地址空間的對映同步。

首先實現一個複製頁表的函式:

// 將 src 頁表的一部分頁對映關係複製到 dst 頁表中。只複製頁表項,不複製實際的物理頁記憶體
// 成功返回0,失敗返回 -1
int
kvmcopymappings(pagetable_t src, pagetable_t dst, uint64 start, uint64 sz) {
    pte_t* pte;
    uint64 pa, i;
    uint flags;

    // PGROUNDUP: 將地址向上取整到頁邊界,防止重新對映已經對映的頁,特別是在執行growproc操作時
    for (i = PGROUNDUP(start);i < start + sz;i += PGSIZE) {
        if ((pte = walk(src, i, 0)) == 0)
            panic("kvmcopymappings: pte should exist");
        if ((*pte & PTE_V) == 0)
            panic("kvmcopymappings: page not present");
        pa = PTE2PA(*pte);

        // `& ~PTE_U` 表示將該頁的許可權設定為非使用者頁
        // 必須設定該許可權,因為RISC-V 中核心是無法直接訪問使用者頁的
        flags = PTE_FLAGS(*pte) & ~PTE_U;
        if (mappages(dst, i, PGSIZE, pa, flags) != 0)
            goto err;
    }

    return 0;

err:
    uvmunmap(dst, PGROUNDUP(start), (i - PGROUNDUP(start)) / PGSIZE, 0);            //解除目標頁表中已對映的頁表項
    return -1;
}

再實現一個縮減記憶體的函式,用於同步核心頁表和使用者頁表:

// 與 uvmdealloc 功能類似,將程式記憶體從 oldsz 縮減到 newsz。但區別在於不釋放實際記憶體,用於核心頁表內程式記憶體對映與使用者頁表程式記憶體對映之間的同步
uint64
kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz) {
    if (newsz >= oldsz)
        return oldsz;

    if (PGROUNDUP(newsz) < PGROUNDUP(oldsz)) {
        int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
        uvmunmap(pagetable, PGROUNDUP(newsz), npages, 0);
    }

    return newsz;
}

xv6核心中,用於對映程式記憶體的地址範圍是[0,PLIC),PLIC:

#define PLIC 0x0c000000L

需要把程序的程式記憶體對映到其核心頁表的這個範圍,首先確認這個範圍內沒有和其他對映衝突。

在xv6手冊中,可以看到這個範圍中有一個CLINT(核心本地中斷器)的對映,這個對映和剛才的說的程式記憶體對映有衝突了。

不過在手冊中也可知,CLINT對映只在核心啟動的時候需要使用,在核心態的使用者程序並不需要使用這個對映。
所以可以在上一個實驗中的kvm_map_pagetable函式中修改一下,把CLINT這個對映去掉:

void kvm_map_pagetable(pagetable_t pgtbl) {
    // 將各種核心需要的 direct mapping 新增到頁表 pgtbl 中
    
    ......

    // CLINT
    // kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);  // CLINT 僅在核心啟動的時候需要使用到,而使用者程序在核心態中的操作並不需要使用到該對映,並且該對映會與要 map 的程式記憶體衝突    

    ......
}

這樣程序的核心頁表中就不會有程式記憶體對映和CLINT對映衝突的問題了。但是這個對映是核心啟動所必須的,所以可以在全域性核心頁表初始化中加上這個對映:

// kernel/vm.c

void
kvminit()
{
    kernel_pagetable = kvminit_newpgtbl(); // 仍然需要有全域性的核心頁表,用於核心 boot 過程,以及無程序在執行時使用。
    kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);     // 全域性核心頁表仍需要對映 CLINT
}

在 exec 中加入檢查,防止程式記憶體超過 PLIC:

int
exec(char *path, char **argv)
{
  ......

  // Load program into memory.
  for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
    if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
      goto bad;
    if(ph.type != ELF_PROG_LOAD)
      continue;
    if(ph.memsz < ph.filesz)
      goto bad;
    if(ph.vaddr + ph.memsz < ph.vaddr)
      goto bad;
    uint64 sz1;
    if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    if(sz1 >= PLIC) // 防止程式記憶體大小超過 PLIC
      goto bad;
    sz = sz1;
    if(ph.vaddr % PGSIZE != 0)
      goto bad;
    if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
      goto bad;
  }
    
  .......
}

之後涉及到使用者態頁表的修改,都要把相應的修改同步到程序的核心頁表中,包括:fork()exec()growproc()userinit()

fork()

int
fork(void)
{
  ......
  // Copy user memory from parent to child.  加入呼叫 kvmcopymappings,將新程序使用者頁表對映複製一份到新程序核心頁表中
  if (uvmcopy(p->pagetable, np->pagetable, p->sz) < 0 || kvmcopymappings(np->pagetable, np->kernelpgtbl, 0, p->sz) < 0) {
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;

  ......
}

exec():

int
exec(char *path, char **argv)
{
	......
  // Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1;
  safestrcpy(p->name, last, sizeof(p->name));

  // 清除核心頁表中對程式記憶體的舊對映,然後重新建立對映
  uvmunmap(p->kernelpgtbl, 0, PGROUNDUP(oldsz) / PGSIZE, 0);
  kvmcopymappings(pagetable, p->kernelpgtbl, 0, sz);

  // Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);

  ......
}

growproc()

// kernel/proc.c

int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if (n > 0) {
      uint64 newsz;
      if ((newsz = uvmalloc(p->pagetable, sz, sz + n)) == 0)
          return -1;

      // 核心頁表中的對映同步擴大
      if (kvmcopymappings(p->pagetable, p->kernelpgtbl, sz, n) != 0) {
          uvmdealloc(p->pagetable, newsz, sz);
          return -1;
      }
      sz = newsz;
  }
  else if (n < 0) {
      uvmdealloc(p->pagetable, sz, sz + n);
      // 核心頁表中的對映同步縮小
      sz = kvmdealloc(p->kernelpgtbl, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

userinit()

void
userinit(void)
{
  ......
  
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;
  kvmcopymappings(p->pagetable, p->kernelpgtbl, 0, p->sz);      // 同步程式記憶體對映到程序核心頁表中

  ......
}

這樣就實現了程序使用者態頁表和核心態頁表的同步

再按照實驗要求替換copyincopyinstr

// kernel/vm.c
// 將 copyin、copyinstr 改為轉發到新函式
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  return copyin_new(pagetable, dst, srcva, len);
}

int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  return copyinstr_new(pagetable, dst, srcva, max);
}

copyin_newcopyinstr已在vmcopyin.c中實現

注意:新新增的函式、修改了傳參的函式要去defs.h中做出對應的調整,不然程式會找不到對應的函式呼叫

到此可以執行make grade,驗證全部實驗是否正確完成

相關文章