首页 > 其他分享 >Mit6.S081笔记Lab3: page tables 页表

Mit6.S081笔记Lab3: page tables 页表

时间:2024-10-26 19:42:23浏览次数:1  
标签:uint64 tables pagetable Mit6 PTE Lab3 页表 进程 内核

课程地址: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,验证全部实验是否正确完成

标签:uint64,tables,pagetable,Mit6,PTE,Lab3,页表,进程,内核
From: https://www.cnblogs.com/Amroning/p/18503660

相关文章

  • Mit6.S081笔记Lab2: system calls 系统调用
    课程地址:https://pdos.csail.mit.edu/6.S081/2020/schedule.htmlLab地址:https://pdos.csail.mit.edu/6.S081/2020/labs/syscall.html我的代码地址:https://github.com/Amroning/MIT6.S081/tree/syscall相关翻译:http://xv6.dgs.zone/labs/requirements/lab2.html参考博客:https:......
  • 中间人攻击(https降级攻击)和iptables命令分析
    中间人攻击以下是一个简单的中间人攻击示例,结合ARP欺骗和流量修改:1.进行ARP欺骗首先,使用 arpspoof 进行ARP欺骗,将受害者的流量重定向到攻击者的机器上:sudoarpspoof-ieth0-t172.29.144.50172.29.144.12.启用IP转发确保IP转发已启用,以便攻击者可以将......
  • 利用iptables实现端口映射(支持动态域名)
    将下列代码保存到/bin/ddns_portmap.sh#!/bin/bash#检查参数if["$#"-ne2];thenecho"Usage:$0<domain><local_port1:remote_port1,local_port2:remote_port2,...>"exit1fi#从参数获取动态域名和端口映射domain=$1port_mappings=$2#获取......
  • k8s和ipvs、lvs、ipvsadm,iptables,底层梳理,具体是如何实现的
    计算节点的功能:提供容器运行的环境kube-proxy的主要功能:术业有专攻,kube-proxy的主要功能可以概括为4个字网络规则那么kube-proxy自己其实是个daemonset控制器跑的每个节点上都有个的pod它负责网络规则其实呢它还是个小领导它不直接去搞网络规则而是告诉别人,网络规......
  • OS-Lab3-Linux进程相关的系统调用和线程相关的库函数
    实验目的1) 演示/实践:Linux进程控制相关的系统调用(如exec()/exit()等)2) 演示/实践:Linux多进程的条件竞争/RaceCondition的现象观察3) 演示/实践:Linux的线程库pthread的初步和入门(如何创建一个用户态的Linux线程:相关库函数pthread_create()等),以及其条件竞争/RaceCondition的现象......
  • IPtables(一)
    规则:filter:包过滤       nat:网络地址转换       mangle:包重构       raw:数据跟踪处理prerouting:路由前规则链      input:数据包入口规则链       forward:转发规则链      output:数据包出口规则链       postrouting......
  • Linux iptables用法与NAT
    1.相关概念2.iptables相关用法3.NAT(DNAT与SNAT)相关概念防火墙除了软件及硬件的分类,也可对数据封包的取得方式来分类,可分为代理服务器(Proxy)及封包过滤机制(IPFilter)。代理服务是一种网络服务,通常就架设在路由上面,可完整的掌控局域网的对外连接。IPFilter这种方式可以直接......
  • Tables for experiments results of two groups under three different types of Rie
    ......
  • iptables配置网络防火墙
    文章目录基本链配置动作常用命令基本链链是数据包传播的途径,每条链对应多条规则。数据包进入服务器需要经过多道关卡,以下是三条应用在“主机防火墙”中的链。INPUT:入口流量OUTPUT:出口流量FORWARD:转发流量配置动作配置防火墙规则时,对于指定的数据包,我们通常可以......
  • datatables使用ajax获取数据
    前端://初始化datatablevartable3=$('.jiaoshi_lst').DataTable({"processing":true,"serverSide":true,"paging":true,"ordering":false,"searching":false......