Lab3-page tables MIT6.1810操作系统工程【持续更新】
Speed up system calls 简单 在这个lab当中要求我们在 xv6 中添加一个新的用户可读的只读内存映射USYSCALL用来让用户态程序在不陷入内核的情况下直接读取部分内核数据如pid并正确处理其创建、映射、访问与释放的完整生命周期。如何将一个用户可读的只读内存映射USYSCALL添加到进程页表内以及如何删除该映射前言和注意事项在xv6当中的有关进程的创建/释放进程页表的创建/释放的过程都在kernel/proc.h,并且按照官网的说法我们需要将进程的pid存放到内存当中这样在调用gitpid系统调用时则直接选择从内存空间当中读取该pid大大提高了执行效率并且不用陷入到内核态这就意味着我们需要在进程的结构体当中添加一个成员用于指向存放当前进程的pid的空间为了之后的读取。一、分配物理内存 前面提到过进程的结构体成员当中有指向进程pid的指针(struct usyscall *)因此我们需要先给他分配物理内存由内核分配。p-usyscall (struct usyscall *)kalloc(); //分配物理内存二、初始化内容 将当前进程的pid存放到刚才的指针p-usyscall所指向的空间中。p-usyscall-pid p-pid; // 以下是xv6提前写好的改进后的ugetpid方法 int ugetpid(void) { struct usyscall *u (struct usyscall *)USYSCALL; //通过虚拟地址USYSCALL访问特点内存 return u-pid; } 为什么我们必须通过struct usyscall *来访问而不是直接返回进程结构体当中的pid呢 答首先xv6有内核页表和用户页表并且用户态下的进程只能看得见内存。因为进程的结构体存放在内核页表当中在用户态下我们只能访问到用户页表所以准确来说我们只能通过虚拟内存搭配页表机制的方式来访问存放在该物理空间当中内容。我们在内核态下通过p-usyscall (struct usyscall *)kalloc();分配的内存似乎也是被内核所管理但是我们将USYSCALL这个虚拟地址和物理地址相映射了起来因此我们可以通过在用户态下访问该虚拟地址的方式下访问到具体的物理地址当中的值。三、创建用户页表 众所周知OS当中的进程采用页表机制来将进程的虚地址映射到物理地址上所以说无论我们是否要添加映射到页表中我们都必不可免地要创建一个用户页表。p-pagetable proc_pagetable(p);四、建立虚拟地址 到 物理地址映射: 说白了就是在用户页表中添加一个新的页表项所以这一步的操作要在页表的相关逻辑当中进行该页表项用于映射到刚才分配的物理内存。在kernel/defs.h当中我们可以看到mappages的声明该函数用于添加映射到页表。 注意页表机制是将进程的虚拟地址映射为内存中真实的物理地址所以在添加新的映射时要一并给出这些参数以及映射大小和权限。// 映射 USYSCALL if(mappages(pagetable, USYSCALL, //虚拟地址 PGSIZE, // 映射大小 (uint64)p-usyscall, //物理地址 PTE_R | PTE_U | PTE_V) 0){ // 官网要求设置的权限 uvmfree(pagetable, 0); return 0; }xv6的权限添加权限的目的是防止“篡改”“非法访问”等等操作位含义PTE_R用户可读PTE_W防止用户写PTE_X防止执行PTE_U用户态可访问PTE_V映射有效五、删除/释放映射: 首先在页表释放的相关逻辑当中进行释放映射的操作在kernel/defs.h当中我们可以看到uvmunmap的声明该函数用于删除/释放映射到页表。uvmunmap(pagetable, USYSCALL, 1, 0); //释放USYSCALL 之后在进程释放的相关逻辑进行释放之前访问的物理空间的操作在kernel/defs.h当中我们可以看到kfree的声明该函数用于释放分配的内存。kfree(p-usyscall);六、深入了解进程和页表的底层逻辑函数kernel/proc.c负责什么allocproc分配“进程资源”pid、usyscall、trapframe、kstack(第一二三步在此进行)freeproc释放“进程资源”第五步后半部分在此进行proc_pagetable构造页表结构第五步前半部分在此进行proc_freepagetable拆除页表结构第四步在此进行 由此我们可以得知页表的生命周期几乎伴随整个进程。代码的相关内容/* kernel/proc.c */ static struct proc* allocproc(void) { struct proc *p; for(p proc; p proc[NPROC]; p) { acquire(p-lock); if(p-state UNUSED) { goto found; } else { release(p-lock); } } return 0; found: p-pid allocpid(); p-state USED; // 分配物理内存 p-usyscall (struct usyscall *)kalloc(); if(p-usyscall 0){ freeproc(p); release(p-lock); return 0; } // 初始化内容 p-usyscall-pid p-pid; // Allocate a trapframe page. if((p-trapframe (struct trapframe *)kalloc()) 0){ freeproc(p); release(p-lock); return 0; } // An empty user page table. p-pagetable proc_pagetable(p); if(p-pagetable 0){ freeproc(p); release(p-lock); return 0; } // 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; } // free a proc structure and the data hanging from it, // including user pages. // p-lock must be held. static void freeproc(struct proc *p) { // 释放之前分配的物理内存 if(p-usyscall){ kfree((void*)p-usyscall); p-usyscall 0; } 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; p-state UNUSED; } // Create a user page table for a given process, with no user memory, // but with trampoline and trapframe pages. pagetable_t proc_pagetable(struct proc *p) { pagetable_t pagetable; // An empty page table. pagetable uvmcreate(); if(pagetable 0) return 0; // 映射 USYSCALL也是关键部分 if(mappages(pagetable, USYSCALL, //虚拟地址 PGSIZE, // 映射大小 (uint64)p-usyscall, //物理地址 PTE_R | PTE_U | PTE_V) 0){ // 权限 uvmfree(pagetable, 0); return 0; } // map the trampoline code (for system call return) // at the highest user virtual address. // only the supervisor uses it, on the way // to/from user space, so not PTE_U. if(mappages(pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) 0){ uvmfree(pagetable, 0); return 0; } // map the trapframe page just below the trampoline page, for // trampoline.S. if(mappages(pagetable, TRAPFRAME, PGSIZE, (uint64)(p-trapframe), PTE_R | PTE_W) 0){ uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; } return pagetable; } // Free a processs page table, and free the // physical memory it refers to. void proc_freepagetable(pagetable_t pagetable, uint64 sz) { uvmunmap(pagetable, USYSCALL, 1, 0); //释放/删除USYSCALL对应的映射 uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmunmap(pagetable, TRAPFRAME, 1, 0); uvmfree(pagetable, sz); }Print a page table 简单 这个lab要求我们实现一个打印页表的函数同时也能帮助我们理解xv6当中页表是如何实现的。在本次实验前这门课程的作者已经将kpgtbl这个系统调用添加到内核当中了现在我们要做的就是完善kernel/vm.c当中的vmprint()函数这个函数接收一个pagetable_t页表类型的参数。xv6当中的页表是怎样的零、专业词汇阐述名称含义VA虚拟地址CPU 使用PTE页表项映射 权限PA物理地址RAM 索引PPN物理页号PA 的高位一、虚拟地址va的结构和xv6当中的三级页表 根据本课程对应的课本xv6 book 当中的第三章我们可以得知在xv6当中虚拟地址va的位数为64位并且我们只使用低39位高25位用于扩展。相信你在看到这里时肯定学过操作系统这门课程在任何一本操作系统的教科书当中对于虚拟地址va的构成的描述都是低n位是页内偏移地址用于定位某页内的页表项剩下的高位都是索引用于定位到某一页。 在xv6当中页表的每页大小为4096B每个页表项PTE的大小为8B所以一个页表的当中有4096/8 512个PTE。所以39位的虚拟地址va当中低12位为页内偏移量省下的27位用于索引页表。 xv6采用三级页表也就是说27位的索引地址每9位构成一个层级类似一个树。以下内容是39位虚拟地址的构成。 PS床图网站随时可能失效所以下面我尽量使用文字来进行描述。|VPN[2] | VPN[1] | VPN[0]|页内偏移| 9 9 9 12 共39位 一级索引 二级索引 三级索引 页内偏移量 总位数 根 叶子 寻址时先访问VPN[2]当中的某个PTE该PTE指向VPN[1]之后从VPN[1]中选取新的PTE再次通过新的PTE寻址VPN[0]用VPN[0]获得最终的PTE后即可获得PNN物理页号。最后通过对PNN操作得到PA物理地址。整个过程类似寻找树的叶子结点那样一层一层向下寻找。二、为什么xv6采用三级页表 进程在创建之初必须且至少拥有一个页表。如果采用一级页表设计为了满足这一必须的条件操作系统必须一次性分配一张覆盖整个虚拟地址空间的页表即使进程只使用其中极小的一部分大部分内存空间会浪费掉也必须遵守该规定。 而在采用三级页表的设计中进程创建时只需要分配一个 4KB 的根页表页其余页表页在虚拟地址空间被实际使用时才按需分配。二、PTE的内容 已知每个PTE的大小为8B即一共64位。其中低10位90位为flags权限位/标记剩下的高位5310位共44bit为PNN物理页框号分配内存之时OS从空闲页框表当中的表头取下来的。最后的10位63~54位暂时未用置为0。 flags的内容位含义V是否有效R可读W可写X可执行U用户可访问A/D硬件访问/修改标记 当一个 PTE 的R/W/X 任一位为 1 时该 PTE 是叶子结点指向真实物理页 若R/W/X 全为 0 且 V1则该 PTE 指向下一级页表。 页表的本质除了指明虚拟地址映射到哪里外还可以决定这个地址是否可读是否可写是否可执行是否可用户态访问/执行。三、PNN如何转为物理地址 xv6当中规定物理地址的位数为56位由PNN和va的低12位拼接而成具体操作手法如下 1、首先讲PTE右移10位这样低10位的flags会消失。 2、之后讲PTE左移12位这样低12位的空白正好可以由虚拟地址的低12位偏移量进行填补。 3、我们现在需要将虚拟地址va的第12位进行填补所以我们将va和0xFFF相与这样va就只剩下了第12位的偏移量。 4、将PTE和va相加或着进行“逻辑或”操作这样就拼接好了一个完整的物理地址。 注意在xv6当中以上的操作都有着对应的宏在编码时可以直接使用宏操作。该lab的实现和代码相关内容一、个人的解析和官网提示打印格式第一行显示 vmprint 的参数。之后每个页表项PTE对应一行包括那些指向树中更深层次页表页的页表项。每个页表项行都缩进若干个 “..”以表示其在树中的深度。每个页表项行都会显示其虚拟地址、页表项位以及从该页表项中提取的物理地址。不要打印无效的页表项。在kernel/riscv.h的文件末尾有关于va转pa的宏。freewalk这个函数也许会带来启发。在printf调用中使用%p以官网上示例所示的方式打印完整的64位十六进制页表项PTE和地址。二、代码相关内容##在kernel/vm.c文件内 static void vmprint_walk(pagetable_t pagetable, int level, uint64 va){ //每个页表521个PTE for(int i 0; i 512; i){ pte_t pte pagetable[i]; // pte有效 并且 V位为1则不是叶子结点 if((pte PTE_V) 0) continue; // 将传入的PA物理地址此时PA第12位为空和偏移量相加合并为完整的物理地址 uint64 newva va | ((uint64)i (12 9 * level)); // 打印层级 depth 2 - level for(int d 0; d 2 - level; d) printf( ..); printf(%p: pte %p pa %p\n, (void*)newva, (void*)pte, (void*)PTE2PA(pte)); // 不是叶子结点则向下递归 if((pte (PTE_R | PTE_W | PTE_X)) 0){ // PTE2PA是将pte转为了物理地址PA此时低12位为空 pagetable_t child (pagetable_t)PTE2PA(pte); vmprint_walk(child, level - 1, newva); } } } #if defined(LAB_PGTBL) || defined(SOL_MMAP) || defined(SOL_COW) void vmprint(pagetable_t pagetable) { // your code here // 打印第一行之后递归进行遍历 printf(page table %p\n, pagetable); vmprint_walk(pagetable, 2, 0); } #endifUse superpages (困难) 这个lab可以说是最难的lab。卡了我快20个小时。当用户通过sbrk()申请内存时如果申请的内存≥2MB时xv6不再使用传统的三级页表即大小为4K的页而是采用二级页表即1个2MB的超级页。并且相关的函数也要适配处理超级页的功能。 采用超级页后的地址结构如下|VPN[2] | VPN[1]包含VPN[0]|页内偏移| 9 9 9 12 共39位 一级索引 二级索引 页内偏移量 总位数 根 叶子 level-2 (512GB) | level-1 (2MB) ← ★ superpage 在这里第一层 | level-0 (4KB) ← 普通的页面在这里第0层 起始该lab的某些地方的写法是有迹可循的你可以直接照搬之前原因的部分代码。顺腾摸瓜寻找需要修改的内容 一、在kernel/kalloc.c文件当中的函数是负责分配页表内存的目前这里只有普通页的内容我们需要添加超级页的相关内容。在kmem中添加一个run结构让其指向一个超级页的空闲页表。 之后在freerange函数当中仿照普通页的内存分配逻辑照葫芦画瓢写一个超级页的内存分配逻辑。同时仿照kfree和kalloc写一个superalloc和superfree这两个分别是超级页的分配和释放。 二、sbrk()当中调用了growproc()函数使用参数n调整内存的大小。当n为有效值时则调用uvmalloc函数来对用户进行虚拟内存的分配这里需要修改uvmalloc。进一步进入uvmalloc函数当中其中涉及了mappages函数该函数负责为每个页表项映射物理地址这里需要修改mappages同时也涉及了uvmdealloc函数该函数的功能是释放用户页面其内部涉及uvmunmap函数这个函数是页面释放的具体实现这里需要修改uvmunmap。在mappages函数当中涉及了walk函数该函数负责返回虚拟地址 va 对应的页表项PTE的地址这里需要修改walk。 三、官网说了通过用户程序pgtbltest来测试超级页功能是否完成所以我们顺藤摸瓜在kernel/pgtbltest.c当中发现superpg_kfork函数调用了fork进程来创建新进程打算让新的进程采用超级页。所以我们再次顺腾摸瓜找到了kfork函数里面涉及了uvmcopy函数这个函数负责将父进程的页表复制给子进程把父进程的数据拷贝一份给子进程这里需要修改uvmcopy。代码相关内容这一小节本人一开始没做出来因此参考了很多大佬的博客和视频才得以做出以下代码参考了这位大佬的博客→mit6.1810]Lab3: page tables。 1、在kalloc.c当中照葫芦画瓢添加对超级页的管理。struct { struct spinlock lock; struct run *freelist; struct run *superfreelist; // 仿照上面的freelist } kmem; void freerange(void *pa_start, void *pa_end) { char *p; p (char*)PGROUNDUP((uint64)pa_start); #ifndef LAB_PGTBL for(; p PGSIZE (char*)pa_end; p PGSIZE) kfree(p); #else int superpg_num 10; // 计算超级页的起始地址从 pa_end 向下对齐到超级页边界 char *superp (char*)SUPERPGROUNDUP((uint64)pa_end - superpg_num * SUPERPGSIZE); // 先释放普通页面部分 for(; p PGSIZE superp; p PGSIZE) kfree(p); // 再释放超级页部分 for(; superp SUPERPGSIZE (char*)pa_end; superp SUPERPGSIZE) superfree(superp); #endif } #ifdef LAB_PGTBL // 超级页释放函数 void superfree(void *pa) { struct run *r; // 参数验证确保 pa 对齐到超级页大小且在合法内存范围内 if(((uint64)pa % SUPERPGSIZE) ! 0 || (char*)pa end || (uint64)pa PHYSTOP) panic(superfree); memset(pa, 1, SUPERPGSIZE); r (struct run*)pa; //加锁 acquire(kmem.lock); r-next kmem.superfreelist; // 将超级页插入空闲链表头部 kmem.superfreelist r; //解锁 release(kmem.lock); } // 超级页分配函数 void * superalloc(void) { struct run *r; acquire(kmem.lock); // 从空闲链表中取出一个超级页 r kmem.superfreelist; if(r) kmem.superfreelist r-next; release(kmem.lock); if(r) memset((char*)r, 5, SUPERPGSIZE); // 返回分配的超级页地址 return (void*)r; } #endif 2、kalloc.h当中我们给普通页分配内存时用到了PGROUNDUP于是超级页的内存分配也需要类似的内容。我们顺腾摸瓜找到riscv.h在里面仿照PGROUNDUP和PGROUNDDOWN新增SUPERPGROUNDUP和SUPERPGROUNDDOWN。#define SUPERPGROUNDUP(sz) (((sz)SUPERPGSIZE-1) ~(SUPERPGSIZE-1)) #define SUPERPGROUNDDOWN(a) (((a)) ~(SUPERPGSIZE-1)) 3、在defs.h中添加下刚才的新增的声明。void * superalloc(void); void superfree(void *pa); pte_t * superwalk(pagetable_t, uint64, int, int *);接下来的内容都在kernel/vm.c当中实现。 4、添加uvmalloc函数。uint64 uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm) { char *mem; uint64 a; int sz; if(newsz oldsz) return oldsz; oldsz PGROUNDUP(oldsz); for(a oldsz; a newsz; a sz){ sz PGSIZE; #ifdef LAB_PGTBL //判断当前大小是否满足使用超级页的开销 if (newsz - a SUPERPGSIZE a % SUPERPGSIZE 0) { //更新大小为超级页方便接下来的递增 sz SUPERPGSIZE; //分配超级页大小的物理内存 mem superalloc(); } else #endif mem kalloc(); if(mem 0){ uvmdealloc(pagetable, a, oldsz); return 0; } #ifndef LAB_SYSCALL memset(mem, 0, sz); #endif //给分配的页添加映射 if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) ! 0){ #ifdef LAB_PGTBL // 如果分配的是超级页大小内存则释放超级页内存 if(sz SUPERPGSIZE) superfree(mem); else #endif kfree(mem); uvmdealloc(pagetable, a, oldsz); return 0; } } return newsz; } 5、修改mappages函数。int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm) { uint64 a, last; pte_t *pte; if((va % PGSIZE) ! 0) panic(mappages: va not aligned); if((size % PGSIZE) ! 0) panic(mappages: size not aligned); if(size 0) panic(mappages: size); a va; last va size - PGSIZE; for (;;) { #ifdef LAB_PGTBL int use_superpage 0; // 用于标识是否使用超级页面映射 // 判断是否可以使用超级页面映射 if ((a % SUPERPGSIZE) 0 (a SUPERPGSIZE last PGSIZE) (perm PTE_U)) { use_superpage 1; // 更改标识 } // 如果是超级页则设置l为1代表接下来在superwalk当中到1层后停止 // 传统的walk会走到level0之后返回pte页表项地址 // 而改进过的superwalk可以被人为操控停到指定的层级。 // 层级从高到底为2 1 0 if (use_superpage) { int l 1; if ((pte superwalk(pagetable, a, 1, l)) 0) return -1; } else { if ((pte walk(pagetable, a, 1)) 0) return -1; } #else // 如果不能使用超级页面映射 就用普通页 if ((pte walk(pagetable, a, 1)) 0) return -1; #endif // 检查PTE是否已经被标记为有效 if (*pte PTE_V) panic(mappages: remap); // 如果有效则将物理地址转换为PTE格式 并加上权限位和有效位 // 这里就是添加映射的核心 *pte PA2PTE(pa) | perm | PTE_V; #ifdef LAB_PGTBL //如果使用超级页 if (use_superpage) { // 则检查是否已经映射到最后一个超级页面 if (a SUPERPGSIZE last PGSIZE) break; // 更新起始地址和物理地址 a SUPERPGSIZE; pa SUPERPGSIZE; } else { if (a last) break; a PGSIZE; pa PGSIZE; } #else //不使用超级页则每次自增一个普通页的大小 if (a last) break; a PGSIZE; pa PGSIZE; #endif } return 0; } 6、修改uvmunmap函数。注意在释放整个页时涉及三种情况页只会在其对应的虚拟地址被完全 unmap 时被释放第一种情况是释放普通页已知每个普通页都是4KB并且xv6的三级页表的最低级也都是4KB所以直接释放即可。第二种情况是释放超级页整块释放超级页的大小为2M因为xv6的三级页表的第二层是表示超级页的层级如果第二层 PTE 是 leaf 并且覆盖 2MB则是超级页此时在地址对其的情况下并且释放该页不会对其它的页造成影响则直接释放即可。第三种情况是释放超级页非整块释放可能比一块小也可能比一块大众所周知在操作系统当中一个 leaf PTE 要么映射整个 4KB 页框要么映射整个 2MB 页框不能只映射其中一部分。所以当我们释放内存时被释放的内存大小没有超过一个超级页 或者 超过了一个超级页那么就必然导致有一个页的完整性被打破从而违反操作系统对单个页完整性的规定。所以我们要将哪些被破坏了完整性的超级页进行降级操作使得其降为普通页。降级的过程就是再开辟新的普通页然后将原来超级页的内容正常存在无需释放的内容复制到新的普通页之后我们删除/释放原来的超级页。问为什么2MB的超级页的完整性被破坏后就必须降级为4KB的普通页答xv6支持3级页表普通页4KB已经是最小的硬件映射粒度不能再细分所以不存在“普通页被部分破坏后再降级”的问题。void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) { uint64 a; pte_t *pte; int sz; if((va % PGSIZE) ! 0) panic(uvmunmap: not aligned); for(a va; a va npages*PGSIZE; a sz){ sz PGSIZE; #ifdef LAB_PGTBL int l 0; // 标志变量 用于确定是超级页还是普通页。 int flag 0; // 标记是否已经处理过超级页 if((pte superwalk(pagetable, a, 0, l)) 0) panic(uvmunmap: walk); #else if((pte walk(pagetable, a, 0)) 0) panic(uvmunmap: walk); #endif if((*pte PTE_V) 0) { printf(va%ld pte%ld\n, a, *pte); panic(uvmunmap: not mapped); } if(PTE_FLAGS(*pte) PTE_V) panic(uvmunmap: not a leaf); /*下面开始解除页面映射*/ if(do_free){ uint64 pa PTE2PA(*pte); // 从页表项中提取物理地址 #ifdef LAB_PGTBL if(l 1) { // 如果是超级页则获取权限 int perm *pte 0xFFF; // 然后清空页表项 *pte 0; // 设置标志 flag 1; // 更新大小为超级页大小 sz SUPERPGSIZE; // 这里是上述的第三种情况如果虚拟地址未对齐到超级页 if(a % SUPERPGSIZE ! 0){ // 对齐到超级页边界 for(uint64 i SUPERPGROUNDDOWN(a); i va; i PGSIZE) { char *mem kalloc(); // 分配新的物理页面 if(mem 0) panic(uvmunmap: kalloc); mappages(pagetable, i, PGSIZE, (uint64)mem, perm); // 将新分配的页面映射到虚拟地址空间 memmove(mem, (char*)pa i - SUPERPGROUNDDOWN(a), PGSIZE); // 将数据从超级页复制到新分配的页面 } a SUPERPGROUNDUP(a); // 更新虚拟地址 sz 0; // 更新大小 } superfree((void*)pa); // 释放超级页 } else #endif // 如果是普通页 kfree((void*)pa); // 释放普通页 } #ifdef LAB_PGTBL if(flag 0) // 避免使用超级页时候被重复清除 #endif *pte 0; } } 7、仿照walk添加superwalk。#ifdef LAB_PGTBL // 参数l用于指定页表的起始级别 pte_t * superwalk(pagetable_t pagetable, uint64 va, int alloc, int *l) { if(va MAXVA) panic(superwalk); for(int level 2; level *l; level--) { // 获取当前层的页表项地址 pte_t *pte pagetable[PX(level, va)]; if(*pte PTE_V) { // 如果页表项有效,将其转为物理地址 pagetable (pagetable_t)PTE2PA(*pte); if(PTE_LEAF(*pte)) { // 如果是叶节点代表找到想要的了更新页表级别返回页表地址。 *l level; return pte; } } else { //页表项无效则尝试分配分配失败返回0 if(!alloc || (pagetable (pde_t*)kalloc()) 0) return 0; // 初始化新分配的页表 memset(pagetable, 0, PGSIZE); // 更新页表项为有效 *pte PA2PTE(pagetable) | PTE_V; } } // 返回目标页表项地址 return pagetable[PX(*l, va)]; } #endif 8、添加uvmcopy函数。int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { pte_t *pte; uint64 pa, i; uint flags; char *mem; int szinc; for(i 0; i sz; i szinc){ szinc PGSIZE; #ifdef LAB_PGTBL int l 0; // 标志变量 用于确定是普通页还是超级页 if((pte superwalk(old, i, 0, l)) 0) // 如果是超级页l1,普通页l0 panic(uvmcopy: pte should exist); #else if((pte walk(old, i, 0)) 0) panic(uvmcopy: pte should exist); #endif if((*pte PTE_V) 0) panic(uvmcopy: page not present); pa PTE2PA(*pte); flags PTE_FLAGS(*pte); #ifdef LAB_PGTBL if(l 1) { // 如果是超级页则将地址增量设置为超级页的大小 szinc SUPERPGSIZE; // 分配超级页大小的内存 if((mem superalloc()) 0) goto err; // 将超级页大小的物理内存从旧地址复制到新分配的内存地址父进程的数据负责给子进程 memmove(mem, (char*)pa, SUPERPGSIZE); // 将超级页大小的新内存映射到新页表的虚拟地址 if(mappages(new, i, SUPERPGSIZE, (uint64)mem, flags) ! 0){ // 释放之前分配的超级页内存 superfree(mem); goto err; } } else { // 如果是普通页 #endif if((mem kalloc()) 0) goto err; memmove(mem, (char*)pa, PGSIZE); if(mappages(new, i, PGSIZE, (uint64)mem, flags) ! 0){ kfree(mem); goto err; } #ifdef LAB_PGTBL } #endif } return 0; err: uvmunmap(new, 0, i / PGSIZE, 1); return -1; }