拉片分析xv6和rcore中的trap和上下文切换机制
学trap感觉非常绕, 浅记一下。个人认为rcore的实现会比较好理解一点, 不过xv6的也值得学习一下, 所有这里把两个的实现都整理了一下。(未开虚拟内存版)
拉片分析: 进程第一次启动 -> 进程第一次返回用户态 -> 进程陷入内核态 -> 进程再返回用户态
rcore对应代码ch3, 未开启分页版, 开启分页后大同小异
- 名词解释
- epc
- 进入异常状态时刻的pc指针
- mret/sret
- 从异常状态返回原来状态(mode), 并根据对应epc设置pc指针
- trapframe/TrapContext
- 下陷时刻的寄存器现场 + 一些切换所需的记录
- 比如下陷时刻
sp应该是用户栈, 所以trapframe.sp保存的就是用户栈
- epc
- 进程第一次启动(初始化)
- 创建进程的上下文context
- 然后调用
swtch函数完成上下文切换 - swtch是个函数调用, 函数返回时会根据ra寄存器返回对应地址
- 为了实现一个完整的闭环, 第一次启动要和第n次下陷的操作是一模一样的, 所以我们要 “伪造”TrapFrame 和 伪造Context(上下文)
- 伪造trapframe:
- trapframe可以理解成下陷时刻用户态的寄存器现场, 我们根据这个现场进入到用户态, 所以我们伪造的trapframe应该
trapframe.sp指向 用户栈trapframe.epc指向 用户程序第一行
- rcore的trapframe是保存在进程内核栈上, 而xv6的trapframe是保存在堆上通过
proc->trapframe访问
- trapframe可以理解成下陷时刻用户态的寄存器现场, 我们根据这个现场进入到用户态, 所以我们伪造的trapframe应该
- 伪造context:
- swtch就是对context的切换, 但是并没有做状态空间切换的操作
- 事实上因为有内核态和用户态, context上下文是有着内核态上下文和用户态上下文两部分的, 这里我们的context结构记录的是内核态上下文, 会一一对应一个trapframe, 哪里保存了用户态上下文
- 所以我们伪造的context应该
context.ra指向用户态返回函数: xv6中是usertrapret, rcore中是__restorecontext.sp指向进程的内核栈
- 伪造trapframe:
- 总结
trapframe.sp指向用户栈trapframe.epc指向用户主程序context.sp指向内核栈- context可以理解为进程的内核态上下文
- trapframe可以理解为进程的内核态上下文
- 进程第一次返回用户态
swtch结束后, 函数返回到ra寄存器指示的地址, xv6中是usertrapret, rcore中是__restore- 此时sp指向内核栈, 我们要找到trapframe来还原用户态现场
- rcore的实现:
- 设置
stvec为用户态下陷的入口地址__alltraps,trap::init() - 因为我们伪造trapframe是在内核栈栈顶, 可以使用使用sp + offset访问
- 切换需要用到一个 “辅助寄存器” sscratch永久保存
- 第一步先让
sscratch指向trapframe.sp即用户栈, 而当前sp是内核栈, 最后一交换栈空间就进入了用户栈, 而sscratch将保存内核栈 - 然后恢复各个寄存器的内容, 弹出内核栈trapframe:
addi sp, sp, 34*8 - 设置sepc为
trapframe.epc即用户主程序 - 最后
sp和sscratch交换完成栈空间切换:csrrw sp, sscratch, sp,sret后pc就成了sepc中的内容
- 设置
- xv6的实现:
- 设置
stvec为用户态下陷的入口地址uservec,w_stvec(uservec); - 设置
sepc为用户态主程序 - 设置trapframe用于还原用户态寄存器现场, 主要是设置
trapframe.kernel_sp, 然后设置trapframe.kernel_trap为trap处理函数 为下次下陷做准备 - 将trapframe通过a0寄存器传递给
userret完成用户态切换 - 与rcore类似, 需要使用
sscratch寄存器做一个中介永久保留 userret第一步让sscratch保存trapframe.a0即用户态a0的值, 然后此时的a0是trapframe- 然后恢复各个寄存器
- 最后
a0和sscratch交换把剩下的一个a0寄存器给恢复了, 之后a0为用户态a0值,sscratch为trapframe
- 设置
- rcore的实现:
- 总结
- rcore的切换是围绕sp(sp是内核栈顶trapframe的引用), sscratch完成的
- xv6的切换是围绕a0(a0是堆中的trapframe的引用), sscratch完成的
- 此时:
- rcore的sscratch指向内核栈栈顶
- xv6的sscratch指向堆中的trapframe
- 进程陷入内核态
- 进入用户态前都设置了
stvec, rcore为__alltraps, xv6为uservec - 上一次返回用户态结束后
- rcore的sscratch指向内核栈栈顶
- xv6的sscratch指向堆中的trapframe
- rcore的实现
- 将下陷时刻的寄存器现场保存到内核栈顶的trapframe
- 所以先从
sscratch中恢复内核栈并开辟栈空间csrrw sp, sscratch, sp,addi sp, sp, -34*8 - 然后保存各个寄存器现场
- 最后将内核栈顶的trapframe传给
trap_handler处理下陷
- xv6的实现
- 先
csrrw a0, sscratch, a0获取到trapframe, 用户态a0保存到sscratch - 然后使用
a0访问trapframe保存一系列寄存器现场 - 因为我们在上一阶段中将trap处理函数保存到了
trapframe.kernel_trap - 所以将
sscratch记录的a0页保存到trapframe中, 最后读出trapframe.kernel_trap到t0,jr t0跳转到trap处理函数
- 先
- 总结
- 找到trapframe(在sscrach寄存器中), 然后存入下陷时刻的寄存器现场到其中
- rcore和xv6最后都在trap处理函数中拿到了
trapframe用于分析处理(读取参数等)- 只不过rcore通过sp定位trapframe, 然后用a0做参数传递
- xv6则是通过结构体定位trapframe, 因为trapframe是在堆上
- 最后都能根据trapframe保存了下陷时刻的用户态现场得到下陷信息
- 进入用户态前都设置了
- 进程再返回用户态
- trap处理函数执行完成后: rcore的
trap_handler, xv6的usertrap - xv6简单再次调用
usertrapret就能返回了 - rcore比较巧妙
- rcore汇编中
__alltrap之后紧接着就是__restore - 所以在
__alltrap调用完trap_handler后就能进入__restore返回用户态 - 而
__restore对初始状态的断言是a0指向内核栈顶的trapframe- 所以在rcore中
trap_handler结束要返回trapframe(rcore中叫TrapContext), 那么根据函数调用协议返回值就会自动保存到a0 - 之后下一条指令就是
__restore形成完美闭环
- 所以在rcore中
- rcore汇编中
- 总结
- 要形成闭环就要满足trap return初始状态的断言
- rcore中断言(
__restore)a0指向内核栈中的trapframe, 所以trap_handler要返回trapframe(TrapContext), 函数调用协议会自动保存到a0中 - xv6中断言(
usertrapret)断言了a0指向trapframe,trapframe.kernel_trap指向trap处理函数- 不过xv6先用
usertrapret过渡把初始状态处理好后再调用userret返回
- 不过xv6先用
- rcore中断言(
- 要形成闭环就要满足trap return初始状态的断言
- trap处理函数执行完成后: rcore的
- 总结
- context相当于进程的内核态上下文现场, trapframe相当于进程的 用户态上下文现场