课程地址: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
Print a page table (easy)
定义一个名为
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*中定义);对copyinstr
和copyinstr_new
执行相同的操作。为每个进程的内核页表添加用户地址映射,以便copyin_new
和copyinstr_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); // 同步程序内存映射到进程内核页表中
......
}
这样就实现了进程用户态页表和内核态页表的同步
再按照实验要求替换copyin
、copyinstr
// 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_new
、copyinstr
已在vmcopyin.c
中实现
注意:新添加的函数、修改了传参的函数要去defs.h
中做出对应的调整,不然程序会找不到对应的函数调用
到此可以执行make grade
,验证全部实验是否正确完成