XV6 0x3
lab3
地址空间
trampoline被映射两次用以跳转和跳出内核。
每个cpu拥有各自的kernel stack,相当于存在各自的kernel线程
- 内核物理地址与虚拟地址相同。
- 设备被映射到低地址
页表
页表保存于内存中,用于将虚拟内存转化为对应的物理内存。
虚拟内存
在xv6里,只使用了39个bit
Offset:12bit,指向4096字节的某一个,此处虚拟内存与物理内存值相同。
L2,L1,L0,分别使用9bit索引表单
SATP寄存器存储页表的物理地址(即L2第0号索引)。
在切换SATP寄存器后要对TLB进行刷新,否则会索引到错误地址
物理内存:
- riscv支持使用56bit索引物理内存
Offset:12bit,指向4096字节的某一个,此处虚拟内存与物理内存值相同。
PPN由L0获得,占44bit。
使用三级页表的优点:
- 极大节省内存空间的同时又不太影响性能。
- 硬件可以帮助我们完成地址翻译,但是我们还是需要
walk函数
转换地址实现内核功能。 - 因为内核虚拟地址和实际的物理地址是相同的,所以通过
walk
页表缓存
处理器都会对于最近使用过的虚拟地址的翻译结果有缓存。这个缓存被称为:Translation Lookside Buffer(TLB)。
TLB在切换页表后要清空,否则地址翻译可能会出错。
Speed up system calls(2021)
注意21年前的lab是没有这个实验的。
主要是要理解创建进程时的内存映射过程,这里搞了很久,最后看题解发现其实很简单…来看一下吧
lab要求是加快getpid()系统调用的速度,那么该怎么加快呢?我们知道,pid在进程的运行周期是不会改变的,因此可以将内存
原:系统调用->syscall()->将参数从用户虚拟地址传送到内核虚拟地址->获取pid->将返回值从内核虚拟地址传送到用户虚拟地址
新:创建进程->将pid写入共享内存->用户调用普通函数(位于ulib.c)即可读取(不必陷入内核)
在memlayout.h
已经定义了usyscall
在虚拟内存中的位置和数据结构
1 |
|
在proc.h
将usyscall添加进结构体,因为进程要监控虚拟内存以及回收虚拟内存
1 | struct usyscall *usyscall; |
在proc.c
先分配一段内存用于存放usyscall
,然后赋值。
注意不能写在p->pagetable = proc_pagetable(p);
前。因为在此函数内,我们需要将分配的内存映射到用户空间,包括我们实现的usyscall
。
1 | static struct proc* |
让我们来编辑proc_pagetable(p)
,可以仿照前面的代码,这里我将USYSCALL映射写在最后,如果映射失败,也需要将前面的成功映射去除。
1 | pagetable_t |
当进程结束时,我们需要取消映射,修改proc_freepagetable
,增加uvmunmap(pagetable, USYSCALL, 1, 0);
1 | void |
当我们去除了映射,就应当free掉内存,在freeproc
释放掉usyscall
1 | static void |
那么我们就完成这个lab,主要是加强了我们对内存页分配流程的理解。
首先要分配一页内存(4096kb),内核填充数据,将内存映射到用户的虚拟内存,此时操作系统和进程虽然有不同的虚拟地址,却能访问相同的数据。
进程结束后,内核取消映射(用户将无法通过虚拟地址访问数据),然后操作系统free内存,腾出空间给下一进程使用。
Print a page table
lab要求是我们打印第一个进程(pid = 1)的虚拟内存空间地址,要求按三层页表结构逐层打印
首先,这是一个内核函数,我们需要在defs.h里填写该函数
1 | // vm.c |
紧接着,我们在exec.c的对应位置添加函数
1 | proc_freepagetable(oldpagetable, oldsz); |
这里借鉴freewalk()
的递归调用,但是由于我们需要记录层数,在内核声明全局变量又不可取,因此这里采用包装函数,在外函数记录层数。
判断层数:
L1、L2:有
效且无
读写执行权限,程序显然不能修改虚拟内存的映射
L0:有
效且有
读写权限,L0属于真实地址,程序需要读写(执行)地址来实现操作。
若有效位PTE_V没有设置,则该页表不存在。
1 | void vm_print_child(pagetable_t pagetable, int level) |
Detect which pages have been accessed
这个lab要求我们判断多个连续内存页中哪个内存页会被修改,返回掩码。
当该内存被访问时,硬件分页步行器会在PTE标记PTE_A,表示该页被访问到。
如图,PTE_A在第6位,我们修改riscv.h文件,定义宏
1 |
紧接着我们利用argaddr
和argint
读入参数,使用walk函数(defs.h未定义,需要extern
)遍历内存地址,然后对返回的pte进行PTE_A
判断,判断后要将PTE_A
置0,防止影响下次调用。
1 |
|
PTE_A标志位的作用:通过PTE_A可以判断是否被经常访问,否则可以放入磁盘节省珍贵的物理内存,内存交换
Simplify
//TODO