【MIT6.S081】Lab3: page tables(详细解答版)

实验内容网址:https://xv6.dgs.zone/labs/requirements/lab3.html
本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree/pgtbl2/

Print a page table

关键点:递归、三级页表

思路:

[图片]

用上图来解释三级页表的原理最为清晰明了。satp的作用是存放根页表页在物理内存中的地址。页表以三级的树型结构存储在物理内存中。该树的根是一个4096字节(512*8byte)的页表页,其中包含512个PTE,每个PTE中包含该树下一级页表页的物理地址。这些页中的每一个PTE都包含该树最后一级的512个PTE(也就是说每个PTE占8个字节,正如图3.2最下面所描绘的)。分页硬件使用27位中的前9位在根页表页面中选择PTE,中间9位在树的下一级页表页面中选择PTE,最后9位选择最终的PTE。一级页表通过stap和L2确定二级页表的基地址,二级页表的基地址加上L1确定三级页表的基地址,三级页表的基地址和L0确定物理地址的前44位,与原来offset的12位组成了物理地址。总体上说,这个过程类似3级512叉树。这样做的目的是为了节省内存,在大范围的虚拟地址没有被映射的常见情况下,三级结构可以忽略整个页面目录。
在每一级页表中,后十位是标志位,在一二级页表中,这些标志位中的RWX是不使用的,一二级页表是起到索引功能,所以只使用了V标志位。

步骤&代码:

  1. kernel/vm.c中定义vmprint()函数,题目要求参数为pagetable_t,但在本题中,需要进行递归,并且递归过程中需要知道当前是递归的第几层,所以需要另外定义一个递归函数, _vmprint(pagetable, level);传递页表指针和递归层数。需要注意的是vmprint()函数需要到def.h文件中声明,_vmprint()函数需要在vmprint()函数前进行定义。
void            
vmprint(pagetable_t pagetable){// 打印根页表printf("page table %p\n", pagetable);// 重新写个函数是为了传递level级和递归_vmprint(pagetable, 1);
}
  1. 编写_vmprint()函数,仿照freewalk函数的遍历方式。通过pte & PTE_V可以判断pte的有效性,在有效的前提下通过 (pte & (PTE_R|PTE_W|PTE_X)) == 0)可以判断是哪一级页表,在第三级页表中,第三级页表存放的是物理地址,页表中页表项中W位,R位,X位起码有一位会被设置为1。根据以上思路编写如下代码:
void _vmprint(pagetable_t pagetable, int level){for(int i = 0; i < 512; i++){pte_t pte = pagetable[i];// 检查pte的有效性if(pte & PTE_V ){// this PTE points to a lower-level page table.uint64 child = PTE2PA(pte);// 打印树的深度for(int j = 0; j < level; j++){if(j==0){printf("..");//第一个..前面不打印空格}else{printf(" ..");}}printf("%d: pte %p pa %p\n",i,pte,child);// 第三级页表存放的是物理地址,页表中页表项中W位,R位,X位起码有一位会被设置为1。如果是索引页表则这些值是0if((pte & (PTE_R|PTE_W|PTE_X)) == 0){_vmprint((pagetable_t)child,level+1);// 还没到第三级,继续递归。}}}
}

A kernel page table per process

前置知识:

原本的xv6系统只有一个内核页表。内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x映射到物理地址仍然是x。每个进程有单独的用户页表,但只包含该进程用户内存的映射,从虚拟地址0开始。内核页表中不含有这些映射,因此用户地址(虚拟地址)在内核中无效,只能通过copyin(),copyoput()等函数将用户地址转化为物理地址再使用。
关于内核栈:

内核栈页面。每个进程都有自己的内核栈,它将映射到偏高一些的地址,这样xv6在它之下就可以留下一个未映射的保护页(guard page)。保护页的PTE是无效的(也就是说PTE_V没有设置),所以如果内核溢出内核栈就会引发一个异常,内核触发panic。如果没有保护页,栈溢出将会覆盖其他内核内存,引发错误操作。恐慌崩溃(panic crash)是更可取的方案。(注:Guard page不会浪费物理内存,它只是占据了虚拟地址空间的一段靠后的地址,但并不映射到物理地址空间。)

如图中的kstack0,1是每个进程的内核栈。/kernel/proc.c文件中的procinit函数中初始化了每个进程的内核栈。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈 ,除了系统调用,像进程切换时的上下文也是保存到内核栈中的。

// 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;}kvminithart();
}

在这里插入图片描述

OK,巴拉巴拉了一大堆,具体的解题过程还需要依靠题目的提示,接下来进入正题。

步骤&代码:

  1. struct proc中为进程的内核页表增加一个字段
struct proc{
...pagetable_t pagetable;       // User page table// 新添加pagetable_t kpt;             // kernel page table 
...
}
  1. 为一个新进程生成一个内核页表的合理方案是实现一个修改版的kvminit,这个版本中应当创造一个新的页表而不是修改kernel_pagetable。你将会考虑在allocproc中调用这个函数。
    我们仿照kvminit重写一个pagetable_t proc_kpt_init()函数,在kvminit函数中,外设的映射是使用kvmmap函数,该函数里面使用了kernel_pagetable,因此我们还需要重写一个void proc_kvmmmap(pagetable_t kpt, uint64 va, uint64 pa, uint64 sz, int perm)函数,将kpt页表指针作为函数参数进行传递。
// 为进程的内核页表新建一个初始化函数
pagetable_t proc_kpt_init(){pagetable_t kpt = (pagetable_t) kalloc();memset(kpt, 0, PGSIZE);// uart registersproc_kvmmmap(kpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);// virtio mmio disk interfaceproc_kvmmmap(kpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);// CLINTproc_kvmmmap(kpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);// PLICproc_kvmmmap(kpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);// map kernel text executable and read-only.proc_kvmmmap(kpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);// map kernel data and the physical RAM we'll make use of.proc_kvmmmap(kpt, (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.proc_kvmmmap(kpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);return kpt;
}// kvmmap是为内核页表的虚拟地址与物理地址做映射,这里需要重新添加一个类似的函数
void proc_kvmmmap(pagetable_t kpt, uint64 va, uint64 pa, uint64 sz, int perm){if(mappages(kpt, va, sz, pa, perm) != 0)panic("proc_kvmmap");
}

函数定义完后记得把函数声明添加到defs.h文件中。
allocproc中调用proc_kpt_init()函数

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;}// ljg add // An empty kernel page table.p->kpt = proc_kpt_init();
...}
  1. 确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在procinit中设置。你将要把这个功能部分或全部的迁移到allocproc
    参照/kernel/proc.c文件中的procinit函数中初始化了每个进程的内核栈,并在allocproc函数中的p->kpt = proc_kpt_init();语句后添加
 // 申请内核栈,确保每一个进程的内核页表都关于该进程的内核栈有一个映射char *pa = kalloc();if(pa == 0)panic("kalloc");uint64 va = KSTACK((int) (p - proc));proc_kvmmmap(p->kpt, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);p->kstack = va;
  1. 修改scheduler()来加载进程的内核页表到核心的satp寄存器(参阅kvminithart来获取启发)。不要忘记在调用完w_satp()后调用sfence_vma()
  2. 没有进程运行时scheduler()应当使用kernel_pagetable
    参照kvminithart函数,在其附近新添加proc_kvminithart函数,以实现传递页表指针。
void
proc_kvminithart(pagetable_t kpt){w_satp(MAKE_SATP(kpt));sfence_vma();
}

然后在scheduler()函数中,进程切换前调用proc_kvminithart()函数。根据“没有进程运行时scheduler()应当使用kernel_pagetable”要求,在进程切换出去–>回来后调用kvminithart()函数。(不懂为什么要在这个时候?)

void
scheduler(void)
{
...p->state = RUNNING;c->proc = p;// 加载进程的内核页表到核心的satp寄存器proc_kvminithart(p->kpt);swtch(&c->context, &p->context);// ljg add Come back to the global kernel page tablekvminithart();// Process is done running for now.// It should have changed its p->state before coming back.c->proc = 0;...}
  1. freeproc中释放一个进程的内核页表
    参照freewalk函数在vm.c文件中添加free_proc_kpt()函数。
// 释放进程的内核页表
void
free_proc_kpt(pagetable_t pagetable)
{// 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){// this PTE points to a lower-level page table.uint64 child = PTE2PA(pte);pagetable[i] = 0;if((pte & (PTE_R|PTE_W|PTE_X)) == 0){// 说明不是第三级,进行递归free_proc_kpt((pagetable_t)child);}} }kfree((void*)pagetable);
}

freeproc()函数中,释放内核栈和内核页表

static void
freeproc(struct proc *p)
{
...if(p->pagetable)proc_freepagetable(p->pagetable, p->sz);p->pagetable = 0;// 释放一个进程的内核栈if(p->kstack){uvmunmap(p->kpt, p->kstack, 1, 1);}p->kstack = 0;// 释放内核页表free_proc_kpt(p->kpt);p->kpt = 0;
...
}
  1. defs.h文件中添加以上函数的声明
void            vmprint(pagetable_t);
pagetable_t     proc_kpt_init();
void            proc_kvmmmap(pagetable_t, uint64 , uint64 , uint64 , int );
void            proc_kvminithart(pagetable_t );
void            free_proc_kpt(pagetable_t pagetable);

进行编译,会发现无法启动系统,报"virtio_disk_intr status"的错误。
原因在于 virtio_disk_rw()函数中为buf申请内核地址时使用了kernel_pagetable,因此要在kvmpa函数中
修改一处地方

uint64
kvmpa(uint64 va)
{uint64 off = va % PGSIZE;pte_t *pte;uint64 pa;pte = walk(myproc()->kpt, va, 0);// 新修改if(pte == 0)panic("kvmpa");if((*pte & PTE_V) == 0)panic("kvmpa");pa = PTE2PA(*pte);return pa+off;
}

进行编译,会出现以下错误。

In file included from kernel/vm.c:9:
kernel/proc.h:87:19: error: field ‘lock’ has incomplete type
87 | struct spinlock lock;
| ^~~~
make: *** [: kernel/vm.o] Error 1

在vm.c中包含头文件即可解决。

#include "spinlock.h"
#include "proc.h"

编译成功后运行usertests,运行通过则本题完成

Simplify

关键点:题目含义

思路:

即使是第二遍做这个题目一开始也不知道怎么入手。哈哈哈
题目需要我将用户空间的映射添加到每个进程的内核页表,将进程的页表复制一份到进程的内核页表就好。
Xv6使用从零开始的虚拟地址作为用户地址空间,而内核的内存从更高的地址开始。然而,这个方案将用户进程的最大大小限制为小于内核的最低虚拟地址,为0xC000000,即PLIC寄存器的地址;

步骤&代码:

  1. vm.c文件中,仿照uvmcopy()函数新建一个复制用户页表映射到每个进程的内核页表映射的函数。uvmcopy()函数是复制父进程的映射到子进程的映射。代码如下:
// 仿照uvmcopy()函数,实现将用户空间的映射添加到每个进程的内核页表
void 
u2k_vmcopy(pagetable_t pagetable, pagetable_t kpt, uint64 oldsz, uint64 newsz){pte_t *pte_from;pte_t *pte_to;oldsz = PGROUNDUP(oldsz);for(uint64 i = oldsz; i < newsz; i += PGSIZE){// 对页表pagetable中虚拟地址为i进行检查,检查pte是否存在if((pte_from = walk(pagetable, i, 0)) == 0)panic("u2k_vmcopy: pte should exist");// 对内核页表kpt中虚拟地址为i进行检查,检查pte是否存在,若不存在则申请物理内存并映射。if((pte_to = walk(kpt, i, 1)) == 0){panic("u2k_vmcopy: pte walk fail");}// 在内核模式下,无法访问设置了PTE_U的页面,// 所以接下来要获得pagetable中虚拟地址为i的pte的标志位// uint64 pa = PTE2PA(*pte_from);// uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);// *pte_to = PA2PTE(pa) | flags;// 感觉上面三句有点多,改成一句*pte_to = (*pte_from) & (~PTE_U);}
}
  1. 根据提示在exec,fork,sbrk函数中添加u2k_vmcopy()函数的调用。
    exec()
int
exec(char *path, char **argv)
{
...uvmclear(pagetable, sz-2*PGSIZE);sp = sz;stackbase = sp - PGSIZE;// 添加复制逻辑u2k_vmcopy(pagetable, p->kpt, 0, sz);// Push argument strings, prepare rest of stack in ustack.for(argc = 0; argv[argc]; argc++) {...
}

fork()

int
fork(void)
{
...np->sz = p->sz;// 复制到新进程的内核页表u2k_vmcopy(np->pagetable, np->kpt, 0, np->sz);np->parent = p;
...
}

sbrk() -> sys_sbrk() -> growproc()函数中,在内存增加时,需要判断一下会不会超过PLIC限制,不超过再复制一份映射到内核页表

int
growproc(int n)
{uint sz;struct proc *p = myproc();sz = p->sz;if(n > 0){// 加上PLIC限制if(PGROUNDUP(sz+n) >= PLIC){return -1;}if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {return -1;}// 复制一份到内核页表u2k_vmcopy(p->pagetable, p->kpt, sz - n, sz);} else if(n < 0){sz = uvmdealloc(p->pagetable, sz, sz + n);}p->sz = sz;return 0;} 
  1. userinit的内核页表中包含第一个进程的用户页表,在这里也需要复制一份
void
userinit(void)
{
...uvminit(p->pagetable, initcode, sizeof(initcode));p->sz = PGSIZE;// 复制一份到内核页表u2k_vmcopy(p->pagetable, p->kpt, 0, p->sz);// prepare for the very first "return" from kernel to user.
...}

记得在defs.h中添加u2k_vmcopy()函数的声明。完毕!

未解:

上述的这几个函数调用的位置可以思考一下,为什么需要在exec,fork,sbrk函数中调用?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/813693.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【windows 】taskkill命令

在Windows操作系统中&#xff0c;并没有直接称为“kill”的命令来终止进程&#xff0c;但是你可以使用“taskkill”命令或者通过任务管理器来实现类似的功能。 “taskkill”是一个强大的命令行工具&#xff0c;可以用来终止一个或多个任务或进程。以下是一些常用的“taskkill”…

智能电网将科技拓展至工厂之外的领域

【摘要/前言】 物联网已然颠覆我们日常生活的许多层面。在家居方面&#xff0c;家电变成连网设备&#xff0c;不仅让我们能控制灯光与上网购物&#xff0c;甚至在出门时提供安全功能。在工业领域&#xff0c;智能工厂改变产品制造的方式。工业物联网(IIoT)不仅让制造商更加敏捷…

Spring Boot 学习(5)——开发流程:快速入门

花了几天的时间&#xff0c;整出个 “hello spring boot”&#xff0c;并且把它从 2 搞到了 3。 纸上得来终觉浅&#xff01;自己实践出真知&#xff01;现在再回头来囫囵一遍&#xff0c;加深下印象。回想下从前自觉某一编程语言大都如此&#xff0c;先找到简单示例照着画一遍…

【vue3-pbstar-books】大学生前端期末作业(vue3、element-plus、ts、pinia、vite、json-server)

一、项目要求 二、项目介绍 vue3-pbstar-books是一个图书主题的pc端网站&#xff0c;该项目有首页、全部书籍页、书籍分类页、书籍详情页和关于页五个页面。该方案结合了 Vue3、Element-Plus、TypeScript、Pinia 和 Vite 等先进技术&#xff0c;实现高效的页面布局、状态管理和…

Spring之AOP的详细讲解

目录 一.SpringAOP是什么&#xff1f; 1.1理论知识点 1.2简单的AOP例子 二.SpringAOP的核心概念 2.1切点(Pointcut) 2.2通知&#xff08;Advice&#xff09; 2.3切⾯(Aspect) 2.4通知类型 2.5切⾯优先级 Order 2.6切点表达式 2.6.1 execution表达式 2.6.2annotati…

天地人和•大道不孤——卢禹舜中国画作品展在重庆美术馆隆重开幕

2024年4月12日&#xff0c;由中国国家画院、重庆市文化和旅游发展委员会主办&#xff0c;重庆美术馆&#xff08;重庆画院、重庆国画院&#xff09;、北京八荒锦绣美术馆、中国国际文化交流基金会卢禹舜艺术基金承办的“天地人和•大道不孤——卢禹舜中国画作品展”开幕式在重庆…

jmeter实验 模拟:从CSV数据到加密请求到解密返回数据再到跨越线程组访问解密后的数据

注意,本实验所说的加密只是模拟加密解密,您需要届时写自己的加解密算法或者引用含有加密算法的相关jar包才行. 思路: 线程组1: 1.从CSV文件读取原始数据 2.将读取到的数据用BeanShell预习处理器进行加密 3.HTTP提取器使用加密后的数据发起请求 4.使用BeanShell后置处理器…

底层文件操作的各种函数(二)------printf,fprintf,sprintf,scanf,fscanf,sscanf的对比以及文件缓冲区

偷得几日清闲&#xff0c;又因一瞬之间对蹉跎时间的愧疚&#xff0c;由此而来到CSDN这个高手云集和新手求学的平台来也写上那么一篇博客。虽然自己的博客那么久不温不热&#xff0c;但坚持写作&#xff0c;巩固自己就好。今天要讲的是续接上一篇文章的补充与继续吧。上期文章&a…

MQ:延迟队列

6.1场景&#xff1a; 1.定时发布文章 2.秒杀之后&#xff0c;给30分钟时间进行支付&#xff0c;如果30分钟后&#xff0c;没有支付&#xff0c;订单取消。 3.预约餐厅&#xff0c;提前半个小时发短信通知用户。 A -> 13:00 17:00 16:30 延迟时间&#xff1a; 7*30 * 60 *…

GB/T 28181标准中的错误码,国标28181中可能出现的SIP协议相关的错误码及其含义

目录 一、GB/T 28181标准介绍 &#xff08;一&#xff09;概述 &#xff08;二&#xff09;关键内容和特点 1. 系统架构&#xff1a; 2. 设备接入&#xff1a; 3. 网络通信&#xff1a; 4. 业务功能&#xff1a; 5. 安全保护&#xff1a; 6. 平台管理&#xff1a; &a…

【C语言】字符串函数和内存函数及其模拟实现

文章目录 前言 一、常见字符串库函数1.strlen函数2.长度不受限制的字符串函数2.1 strcpy2.2 strcat2.3 strcmp 3.长度受限制的字符串函数3.1 strncpy3.2 strncat3.3 strncmp 二、字符串查找函数strstrstrtok 三、strerror函数四、内存操作函数1.memcpy2.memmove3.memcmp 五、字…

力扣刷题 二叉树层序遍历相关题目II

NO.116 填充每个节点的下一个右侧节点指针 给定一个 完美二叉树 &#xff0c;其所有叶子节点都在同一层&#xff0c;每个父节点都有两个子节点。二叉树定义如下&#xff1a; struct Node {int val;Node *left;Node *right;Node *next; } 填充它的每个 next 指针&#xff0c;…

iOS开发如何更改xcode中的Apple ID

在Xcode中更改Apple ID是一项常见的任务&#xff0c;尤其是当你需要切换到另一个开发者账号或者团队时。下面是一个简单的步骤指南&#xff0c;帮助你更改Xcode中的Apple ID&#xff1a; 步骤一&#xff1a;退出当前的Apple ID 1.打开Xcode应用程序。 2.在菜单栏中&#xff0c;…

外贸公司应该怎么选择企业邮箱?哪个企业邮箱最好?

外贸公司业务的特殊性需要他们频繁进行跨国的沟通交流&#xff0c;那么外贸公司应该如何选择适合的企业邮箱呢&#xff1f;首先&#xff0c;传输邮件的稳定安全是前提&#xff0c;另外由于沟通多是国外客户&#xff0c;邮件的翻译也成为外贸公司企业邮箱的刚需。小编今天就详细…

ARMv8-A架构下的外部debug模型之外部调试事件(external debug events)概述

外部调试器与处理器之间的握手与external debug events 一&#xff0c;External Debug的使能二&#xff0c;外部调试器和CPU之间的握手三&#xff0c;外部调试事件 External debug events1. External debug request event2. Halt instruction debug event3. Halting step debug…

【从浅学到熟知Linux】环境变量详谈(含使用程序获取环境变量的3种方法、如何查看环境变量)

&#x1f3e0;关于专栏&#xff1a;Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程及数据库等内容。 &#x1f3af;每天努力一点点&#xff0c;技术变化看得见 文章目录 环境变量基本概念查看环境变量的方法环境变量相关命令环境变量组织方式及获取环境变量的3种方法验…

Cesium.js--》探秘Cesium背后的3D模型魔力—加载纽约模型

今天简单实现一个Cesium.js的小Demo&#xff0c;加强自己对Cesium知识的掌握与学习&#xff0c;先简单对这个开源库进行一个简单的介绍吧&#xff01; Cesium 是一个开源的地理空间可视化引擎&#xff0c;用于创建基于 Web 的三维地球应用程序。它允许开发人员在网页上呈现高度…

Java基础第十一课——类与对象(2)

由于类与对象这一部分的知识点很多&#xff0c;而且操作方法也有很多&#xff0c;所以这次将继续深入讨论一下关于类与对象中方法传参、方法重载、构造方法以及this关键字使用方面的知识。 一、方法传参 1.return关键字 return关键字作用 作用场景&#xff1a;方法内 作用…

天猫精灵要会员,不能听歌,还能用来干什么呢?榨干它的剩余价值

目录 起因&#xff1a;以听歌为主要功能的设备&#xff0c;却不能听歌了 1.蓝牙音箱 2.控制智能家电 3.万能遥控器&#xff0c;需要一个外接设备 4.倒计时/提醒&#xff0c;闹钟提醒&#xff0c;整点提醒&#xff08;这功能有人不喜欢&#xff0c;闲吵&#xff0c;还不能关…

LeetCode题练习与总结:最小路径和--64

一、题目描述 给定一个包含非负整数的 m x n 网格 grid &#xff0c;请找出一条从左上角到右下角的路径&#xff0c;使得路径上的数字总和为最小。 说明&#xff1a;每次只能向下或者向右移动一步。 示例 1&#xff1a; 输入&#xff1a;grid [[1,3,1],[1,5,1],[4,2,1]] 输出…