继续上一篇的 context switch 的调试。系统调用的过程后面再详细说明,这里就返回结果了。
0x02 内核空间切换到用户空间
当前的函数调用关系如下:
syscall 的返回值写到了 trapframe 中的 a0,此时已经完成了hello.c内write函数的打印任务 可以看到qemu控制台上已经显示了字符a和回车。
完成后返回到 usertrapret() 函数,它是返回用户空间的必经过程。主要完成下面几件事情:
(1) 首先关闭中断。之前系统调用syscall函数的时候打开的中断,现在又到了操作系统 的关键阶段,状态很微妙,不能被打扰,所以关闭中断;
(2) 保存 p->trapframe->kernel_satp = r_satp()
,将当前的satp页表首地址保存起来,
这是在返回用户空间需要保存内核页表地址;
(3) p->trapframe->kernel_sp = p->kstack + PGSIZE
,保存用户进程的内核线程堆栈空间
(4) p->trapframe->kernel_trap = (uint64)usertrap
保存usertrap的地址,这个地址是
内核页表下的usertrap地址,其实也是物理地址;其实satp和usertrap每个进程的trapframe
保存的值都是一样的。
(5) p->trapframe->kernel_hartid = r_tp()
, 这里为啥要保存tp寄存器?tp中保存了当前
hartid,但是用户进程是不能访问mhartid的,所以将这个值保存到tp中。给他保存在trapframe
中使为了让用户进程知道自己运行在哪个hartid中。另外,scheduler 选择进程运行的时候,
需要将scheduler线程运行的hartid写到进程trapframe中的kernel_hartid
中。
(6) 修改 sstatus 寄存器的 SPP 值,准备返回U-mode;
(7) 准备好用户页表地址,作为 userret 函数的参数,进入到 userret
userret 是返回 U-mode 的最后阶段。userret 在内核空间中有两个地方,1份作为.text保存 在了内核代码段,另1份是映射到了 trampoline 页,和用户空间共享。这里运行的必须是第二份, 否则无法切换到U-mode。
userret相对比较简单。进入userret 就开始切换 satp 到用户进程的页表。
# switch to the user page table.
sfence.vma zero, zero
csrw satp, a0
sfence.vma zero, zero
然后从内存中恢复用户进程使用的所有寄存器,最后调用sret 返回到用户进程。
sret 返回之后会切换到 0x2f6 地址,从 user/hello.asm 中可以看到,马上就可以返回到 ecall 之后的ret了,然后再返回到 hello.c 。
这样就完成了一次完整的用户空间通过系统调用陷入内核,并返回到用户空间。这里没有详细
说内核函数sys_write
的执行过程,后续在补充吧。