page tables
学习page table,然后够魔改使能够实现用户空间到内核空间的复制。
kern/memlayout.h内存布局kern/vm.c虚拟内存相关代码kernel/kalloc.c动态分配物理内存
print a page table
实现vmprint(pagetable_t pt)打印页表的内容。在exec.c:exec()的return argc;前插入if(p->pid==1) vmprint(p->pagetable),打印第一个进程的页表。
测试:make grade的printout
打印格式如下:
- 第一行是
vmprint的参数 - 每行打印页表项号,页表项内容和得出的物理地址
- 用
..表示几级页表 - 不打印无效页表项
- 结果虚拟地址相同但物理地址可能不同
1 | page table 0x0000000087f6e000 |
虚拟内存
页表布局:
1 | | PTE | |
页表使用pagetable_t管理,是一个uint64*,可以使用数组方式访问每个ptept[i]。然后可以使用位运算操作检测pte的标志pte & PTE_V
因为以页面为单位进程分配,而每个页面4KB。所以页表的物理地址是页对齐的,这就是为什么#define PTE2PA(pte) (((pte) >> 10) << 12)要左移动12bit(4K)。最终以pa + offset使用。而PTE的低10bit作为标志位使用。
详见手册Figure 3.2: RISC-V address translation details
hint
- 声明定义
vmprint - 使用
kernel/riscv.h中工具宏 - 可以参考
freewalk- 需要注意:
freewalk中所有叶子页表都已经移除所以判断递归的方式有所不同 - 这里直接采用三重循环打印三级页表
- 需要注意:
解释说明page0, page2的内容,以及用户空间程序是否可以读写page1?
收获
- 熟悉riscv虚拟内存机制
satp一级页表布局 - 学习操作页表
A kernel page table per process
xv6中每个进程有一个对应的内核页,vx6的内核页也是启用虚拟内存的, 只不过内核表和用户页表隔离(要对不同根页表查表)。然后使用分页的方式提供用户空间内存。因为内核页表和用户页表是隔离的,所以要在内核太中使用用户空间的指针需要先进行手动地址翻译。
- 修改内核,让每个进程在内核态时都使用自己的内核页表
- 在
struct proc结构中添加记录 - 修改调度器,添加内核页表切换的功能
- 在
- 测试
usertests
hint
- 在
struct proc中添加用于记录内核页表成员 - 创建一个功能类似
kvminit的函数,用于创建新进程时(allocproc)生成内核页表 - 在原始xv6中所有内核栈是在
procinit中初始化的,所以进程独立内核页应该对此修改,在allocproc中实现- 注意内核页表中映射内核栈
- 修改
scheduler()以切换内核页表- 页表地址放入
satp寄存器(可以参考kvminithart) w_satp()后使用sfence_vma()清理指令缓存
- 页表地址放入
- 在没有进程运行时
scheduler使用kernel_pagetable- 调度器
switch()返回时切换内核页表(即switch后的指令),因为调度器在内核态,switch返回说明进入内核态使用内核栈
- 调度器
- 在
freeproc实现内核页表释放 - ?为何要这样? 实现一个方法仅释放当前页表,而不释放其叶子物理页
- 调试tips,缺页会导致
sepc=0x00000000XXXXXXXX可以通过该xxx地址查找kernel/kernel.asm
收获
- vm的内核页表也不神秘嘛,就一个page(然后虚拟内存多级页表),然后响应成员记录一下
- 每个进程的内核页表,在内核页表上映射内核栈
kalloc分配物理内存- xv6项目结构,原型声明等
- 切换页表需要使用
sfence_vma()清理指令缓存,这也是为什么切换代价那么大的原因 - 内核页表和用户进程页表独立的,要知道用户进程映射到内核是为了减少地址翻译的开销。所以在释放内核页表时要考虑用户页表是需要释放还是至少不映射
- 用
satp寄存器切换页表 - 我们释放内核页表仅是解除映射和释放”中介索引页”不会释放物理页(物理页由释放用户程序部分释放),而内核栈申请到物理页后映射到内核页表中。释放程序内核需要释放内核页表前完成内核栈的释放工作,否则就无法获取内核栈物理地址
Simplify copyin/copyinstr
原始版本的copyin函数要在内核空间访问用户空间的地址,而内核页表和用户页表是隔离的,所以需要在内核手动翻译(walk)成物理地址。
现在要将用户空间的内存映射到内核页表中,这样就可以直接在内核态解引用指针。实现copyin和string版本copyinstr。
- 用
kernel/vmcopy.c:copyin_new()的调用替换原kernel/vm.c:copyin()函数体。coypin_new就是使用直接解引用的版本- 同理
copyinstr_new替换copyinstr
- 同理
- 将用户空间地址映射到进程的内核空间
- 测试
make grade和usertests - 注意映射到空闲的区域
- xv6中用户空间虚拟地址在地处,内核空间在高位,所以只需关注上限这里是
PLIC寄存器地址
- xv6中用户空间虚拟地址在地处,内核空间在高位,所以只需关注上限这里是
- 内核booted后地址是
0xC000000(plic寄存器)- 详见
kvminit(),memlayout.h和Figure 3-4 - 需要修改xv6防止用户程序地址映射到大于plic地址的内核空间
- 详见
hints
因为内核页表和用户页表的隔离的,查表得到的物理页可能不同。映射到内核空间本质上就是让相同的虚拟地址能够寻址到相同的物理页。虚拟地址通过walk查询并开辟,最后在叶子节点处写入物理页号就完成了映射。
- 函数体替换成对
copyin_new的调用 - 当映射发生改变,要在内核页表中做响应的改变。如
fork,exec和sbrk- 其中
sbrk调用growproc
- 其中
- 在
userinit中对初始程序做映射- 因为会对
fork处理,又因为所有程序都是初始程序的子程序,所以其他程序都会用到这个机制
- 因为会对
- 映射到内核空间的页表需要
~PTE_U的特权级,即清除PTE_U标志位 - 注意上述plic限制
这个机制减少了内核态到用户态的页表切换,但是带来了Meltdown和Spectre漏洞
解释为什么在第三个测试中
srcva + len < srcva在copyin_new()中是必要的。写出srcva和len的值,在前两个测试中是失败的,但是在第三个测试中是成功的。为了
uint64的srcva溢出
bug
usertestsremap,因为内核页表初始化时映射了CLINT,而usertests时需要用到那块地址。CLINT是启动时时钟中断相关的寄存器,在机器级使用,所以不需要在进程内核页表中映射
test reparent2bug- 释放没做好申请不到空间了
- 包括解除映射和释放中介页表
- 释放所有中介页表,但不释放物理页
- 此外释放内核页虽然不释放用户管理物理内存,但是内核栈属于内核管理需要释放
- 可以参考
walk
panic: kerneltrapswitch()返回后kvminithart()换回内核页表- 因为内核态发生调度执行用户程序切换了用户页表,当调度返回回到内核态时需要从用户页表换回内核页表
- 释放没做好申请不到空间了
test sbrkbugs,sbrkbasic- sbrk地址空间的伸缩涉及到重复映射和解除未映射页面的映射的问题
- 可以考虑下面的例子,思路是向上取整
如何解决非页对齐的sbrk!!,处理不好会带来要么解除映射未映射页面或重复映射的情况
- 向上取整PGROUNDUP,因为分配是以页为单位的,1.0001页也是两页
- 所以在映射到内核空间时思考向上取整的问题才不会
remap和unmap invalid - 如下两个例子加上思考向上取整方便理解(数字的单位为4KB)
- 3.3(单位4KB)缩小到2.1,因为分配单位是页,所以页1, 页2保留, 页3 unmap
- 2.3 增长到 3.5,页为分配单位那么第2页必定是已经映射的,所以只需再映射页3
需要注意的是其他程序可以让另一个程序释放内存
这里假设想要释放内核栈空间,ok哪拿到kstack的物理地址然后调用kfree释放。我原本是使用kvmpa(va)来获取,而问题就出在我的kvmpa()实现上,我默认差的内核页表是myproc()->kernel_pagetable,这就出了问题,因为空间是会被别的程序释放的!所以myproc()->kernel_pagetable并不是我要释放的内核栈所在页表。